@cloudillo/canvas-tools

Overview

React components and hooks for interactive object manipulation in SVG canvas applications. Provides transform gizmos, rotation handles, pivot controls, and gradient pickers for building drawing and design tools.

Installation

pnpm add @cloudillo/canvas-tools

Peer Dependencies:

  • react >= 18
  • react-svg-canvas

Components

TransformGizmo

Complete transform control for SVG objects with rotation, scaling, and positioning.

import { TransformGizmo } from '@cloudillo/canvas-tools'

function CanvasEditor() {
  const [bounds, setBounds] = useState({ x: 100, y: 100, width: 200, height: 150 })
  const [rotation, setRotation] = useState(0)

  return (
    <svg width={800} height={600}>
      <TransformGizmo
        bounds={bounds}
        rotation={rotation}
        onBoundsChange={setBounds}
        onRotationChange={setRotation}
        showRotationHandle
        showPivotHandle
      />
    </svg>
  )
}

Props (TransformGizmoProps):

  • bounds: Bounds - Object position and size { x, y, width, height }
  • rotation?: number - Rotation angle in degrees
  • pivot?: Point - Pivot point { x, y }
  • onBoundsChange?: (bounds: Bounds) => void - Bounds change callback
  • onRotationChange?: (angle: number) => void - Rotation change callback
  • onPivotChange?: (pivot: Point) => void - Pivot change callback
  • showRotationHandle?: boolean - Show rotation arc handle
  • showPivotHandle?: boolean - Show pivot point control

RotationHandle

Circular arc handle for rotating objects.

import { RotationHandle } from '@cloudillo/canvas-tools'

<RotationHandle
  center={{ x: 200, y: 200 }}
  radius={80}
  currentAngle={rotation}
  onRotate={(angle) => setRotation(angle)}
  snapAngles={[0, 45, 90, 135, 180, 225, 270, 315]}
/>

Props (RotationHandleProps):

  • center: Point - Center point of rotation
  • radius: number - Arc radius
  • currentAngle: number - Current rotation angle
  • onRotate: (angle: number) => void - Rotation callback
  • snapAngles?: number[] - Angles to snap to (default: 45-degree increments)
  • snapZoneRatio?: number - Snap zone size ratio

PivotHandle

Draggable handle for setting the pivot/rotation center point.

import { PivotHandle } from '@cloudillo/canvas-tools'

<PivotHandle
  position={{ x: 200, y: 200 }}
  bounds={{ x: 100, y: 100, width: 200, height: 200 }}
  onPivotChange={(point) => setPivot(point)}
  snapToCenter
  snapThreshold={10}
/>

Props (PivotHandleProps):

  • position: Point - Current pivot position
  • bounds: Bounds - Object bounds for snapping
  • onPivotChange: (point: Point) => void - Position change callback
  • snapToCenter?: boolean - Snap to center when close
  • snapThreshold?: number - Snap distance threshold

GradientPicker

Complete gradient editor with color stops, angle control, and presets.

import { GradientPicker } from '@cloudillo/canvas-tools'
import type { Gradient } from '@cloudillo/canvas-tools'

function GradientEditor() {
  const [gradient, setGradient] = useState<Gradient>({
    type: 'linear',
    angle: 90,
    stops: [
      { offset: 0, color: '#ff0000' },
      { offset: 1, color: '#0000ff' }
    ]
  })

  return (
    <GradientPicker
      value={gradient}
      onChange={setGradient}
      showPresets
      showAngleControl
    />
  )
}

Props (GradientPickerProps):

  • value: Gradient - Current gradient value
  • onChange: (gradient: Gradient) => void - Change callback
  • showPresets?: boolean - Show gradient preset grid
  • showAngleControl?: boolean - Show angle rotation control
  • showPositionControl?: boolean - Show radial gradient position control

GradientBar

Horizontal bar for editing gradient color stops.

import { GradientBar } from '@cloudillo/canvas-tools'

<GradientBar
  stops={gradient.stops}
  onStopsChange={(stops) => setGradient({ ...gradient, stops })}
  selectedStop={selectedIndex}
  onSelectStop={setSelectedIndex}
/>

GradientPresetGrid

Grid of predefined gradient presets.

import { GradientPresetGrid, GRADIENT_PRESETS } from '@cloudillo/canvas-tools'

<GradientPresetGrid
  presets={GRADIENT_PRESETS}
  onSelect={(preset) => setGradient(expandGradient(preset.gradient))}
  category="warm"
/>

AngleControl

Circular control for setting gradient angle.

import { AngleControl, DEFAULT_ANGLE_PRESETS } from '@cloudillo/canvas-tools'

<AngleControl
  value={angle}
  onChange={setAngle}
  presets={DEFAULT_ANGLE_PRESETS}
/>

PositionControl

XY position control for radial gradient centers.

import { PositionControl } from '@cloudillo/canvas-tools'

<PositionControl
  value={{ x: 0.5, y: 0.5 }}
  onChange={setPosition}
/>

Hooks

useTransformGizmo

Hook for managing transform gizmo state and interactions.

import { useTransformGizmo } from '@cloudillo/canvas-tools'

function CanvasObject({ object, onUpdate }) {
  const {
    state,
    handlers,
    isDragging,
    isRotating,
    isResizing
  } = useTransformGizmo({
    bounds: object.bounds,
    rotation: object.rotation,
    pivot: object.pivot,
    onBoundsChange: (bounds) => onUpdate({ ...object, bounds }),
    onRotationChange: (rotation) => onUpdate({ ...object, rotation }),
    onPivotChange: (pivot) => onUpdate({ ...object, pivot }),
    snapAngles: [0, 45, 90, 135, 180, 225, 270, 315],
    maintainAspectRatio: true
  })

  return (
    <g {...handlers}>
      {/* Object rendering */}
    </g>
  )
}

Options (TransformGizmoOptions):

  • bounds: Bounds - Initial bounds
  • rotation?: number - Initial rotation
  • pivot?: Point - Initial pivot
  • onBoundsChange?: (bounds: Bounds) => void
  • onRotationChange?: (angle: number) => void
  • onPivotChange?: (pivot: Point) => void
  • snapAngles?: number[] - Rotation snap angles
  • snapZoneRatio?: number - Snap sensitivity
  • maintainAspectRatio?: boolean - Lock aspect during resize
  • minWidth?: number - Minimum width constraint
  • minHeight?: number - Minimum height constraint

Returns (UseTransformGizmoReturn):

  • state: TransformGizmoState - Current transform state
  • handlers: TransformGizmoHandlers - Event handlers
  • isDragging: boolean - Move operation active
  • isRotating: boolean - Rotation operation active
  • isResizing: boolean - Resize operation active

Gradient Utilities

Creating Gradients

import {
  DEFAULT_LINEAR_GRADIENT,
  DEFAULT_RADIAL_GRADIENT,
  expandGradient,
  compactGradient
} from '@cloudillo/canvas-tools'

// Start with defaults
const linear = { ...DEFAULT_LINEAR_GRADIENT }
const radial = { ...DEFAULT_RADIAL_GRADIENT }

// Expand compact notation to full gradient
const full = expandGradient({ t: 'l', a: 90, s: [[0, '#f00'], [1, '#00f']] })

// Compact for storage
const compact = compactGradient(full)

Manipulating Stops

import {
  addStop,
  removeStop,
  updateStop,
  sortStops,
  reverseStops
} from '@cloudillo/canvas-tools'

// Add a stop at 50%
const newStops = addStop(gradient.stops, 0.5, '#00ff00')

// Remove stop at index 1
const filtered = removeStop(gradient.stops, 1)

// Update stop color
const updated = updateStop(gradient.stops, 1, { color: '#ff00ff' })

// Sort by offset
const sorted = sortStops(stops)

// Reverse direction
const reversed = reverseStops(stops)

Converting to CSS/SVG

import {
  gradientToCSS,
  createLinearGradientDef,
  createRadialGradientDef
} from '@cloudillo/canvas-tools'

// CSS background value
const css = gradientToCSS(gradient)
// "linear-gradient(90deg, #ff0000 0%, #0000ff 100%)"

// SVG gradient definition
const svgLinear = createLinearGradientDef('myGradient', gradient)
const svgRadial = createRadialGradientDef('myGradient', gradient)

Color Utilities

import {
  interpolateColor,
  getColorAtPosition
} from '@cloudillo/canvas-tools'

// Blend two colors
const mixed = interpolateColor('#ff0000', '#0000ff', 0.5)
// "#800080"

// Get color at position in gradient
const colorAt25 = getColorAtPosition(gradient.stops, 0.25)

Geometry Utilities

Coordinate Transformation

import {
  getCanvasCoordinates,
  getCanvasCoordinatesWithElement,
  getSvgElement
} from '@cloudillo/canvas-tools'

function handleClick(event: MouseEvent) {
  const svg = getSvgElement(event.target as Element)
  const point = getCanvasCoordinates(event, svg)
  console.log('Canvas position:', point.x, point.y)
}

Rotation Matrix

Pre-calculated trigonometry for performance-critical operations.

import {
  createRotationMatrix,
  rotatePointWithMatrix,
  unrotatePointWithMatrix,
  rotateDeltaWithMatrix,
  unrotateDeltaWithMatrix
} from '@cloudillo/canvas-tools'

// Create matrix once
const matrix = createRotationMatrix(45) // 45 degrees

// Rotate points efficiently
const rotated = rotatePointWithMatrix({ x: 100, y: 0 }, matrix, center)
const original = unrotatePointWithMatrix(rotated, matrix, center)

// Rotate deltas (movement vectors)
const rotatedDelta = rotateDeltaWithMatrix({ x: 10, y: 0 }, matrix)

View Coordinates

import {
  canvasToView,
  viewToCanvas,
  isPointInView,
  boundsIntersectsView
} from '@cloudillo/canvas-tools'

// Convert between canvas and view coordinates
const viewPoint = canvasToView(canvasPoint, viewTransform)
const canvasPoint = viewToCanvas(viewPoint, viewTransform)

// Visibility checks
const visible = isPointInView(point, viewport)
const intersects = boundsIntersectsView(objectBounds, viewport)

Resize Calculations

import {
  initResizeState,
  calculateResizeBounds,
  getAnchorForHandle,
  getRotatedAnchorPosition
} from '@cloudillo/canvas-tools'

// Initialize resize operation
const resizeState = initResizeState(
  bounds,
  rotation,
  'se', // corner handle
  startMousePosition
)

// Calculate new bounds during drag
const newBounds = calculateResizeBounds(
  resizeState,
  currentMousePosition,
  { maintainAspectRatio: true }
)

Types

Core Types

import type {
  Point,
  Bounds,
  ResizeHandle,
  RotationState,
  PivotState,
  RotatedObjectBounds,
  TransformedObject,
  RotationMatrix,
  ResizeState
} from '@cloudillo/canvas-tools'

type Point = { x: number; y: number }

type Bounds = {
  x: number
  y: number
  width: number
  height: number
}

type ResizeHandle = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'

Gradient Types

import type {
  GradientType,
  GradientStop,
  Gradient,
  CompactGradient,
  GradientPreset,
  GradientPresetCategory
} from '@cloudillo/canvas-tools'

type GradientType = 'linear' | 'radial'

type GradientStop = {
  offset: number  // 0-1
  color: string   // hex color
}

type Gradient = {
  type: GradientType
  angle?: number        // linear: degrees
  cx?: number           // radial: center x (0-1)
  cy?: number           // radial: center y (0-1)
  stops: GradientStop[]
}

type CompactGradient = {
  t: 'l' | 'r'          // type
  a?: number            // angle
  cx?: number
  cy?: number
  s: [number, string][] // stops as tuples
}

Constants

Rotation Defaults

import {
  DEFAULT_SNAP_ANGLES,
  DEFAULT_SNAP_ZONE_RATIO,
  DEFAULT_PIVOT_SNAP_POINTS,
  DEFAULT_PIVOT_SNAP_THRESHOLD
} from '@cloudillo/canvas-tools'

// [0, 45, 90, 135, 180, 225, 270, 315]
console.log(DEFAULT_SNAP_ANGLES)

// 0.1 (10% of arc)
console.log(DEFAULT_SNAP_ZONE_RATIO)

Arc Sizing

import {
  ARC_RADIUS_MIN_VIEWPORT_RATIO,
  ARC_RADIUS_MAX_VIEWPORT_RATIO,
  DEFAULT_ARC_PADDING,
  calculateArcRadius
} from '@cloudillo/canvas-tools'

const radius = calculateArcRadius({
  objectBounds: bounds,
  viewportSize: { width: 800, height: 600 },
  padding: DEFAULT_ARC_PADDING
})

Angle Presets

import { DEFAULT_ANGLE_PRESETS } from '@cloudillo/canvas-tools'

// [0, 45, 90, 135, 180, 225, 270, 315]
console.log(DEFAULT_ANGLE_PRESETS)

Gradient Presets

import {
  GRADIENT_PRESETS,
  getPresetsByCategory,
  getPresetById,
  getCategories
} from '@cloudillo/canvas-tools'

// Get all categories
const categories = getCategories()
// ['warm', 'cool', 'vibrant', 'subtle', 'monochrome']

// Get presets in a category
const warmGradients = getPresetsByCategory('warm')

// Get specific preset
const sunset = getPresetById('sunset')

See Also