@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-toolsPeer Dependencies:
react>= 18react-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 degreespivot?: Point- Pivot point{ x, y }onBoundsChange?: (bounds: Bounds) => void- Bounds change callbackonRotationChange?: (angle: number) => void- Rotation change callbackonPivotChange?: (pivot: Point) => void- Pivot change callbackshowRotationHandle?: boolean- Show rotation arc handleshowPivotHandle?: 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 rotationradius: number- Arc radiuscurrentAngle: number- Current rotation angleonRotate: (angle: number) => void- Rotation callbacksnapAngles?: 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 positionbounds: Bounds- Object bounds for snappingonPivotChange: (point: Point) => void- Position change callbacksnapToCenter?: boolean- Snap to center when closesnapThreshold?: 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 valueonChange: (gradient: Gradient) => void- Change callbackshowPresets?: boolean- Show gradient preset gridshowAngleControl?: boolean- Show angle rotation controlshowPositionControl?: 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 boundsrotation?: number- Initial rotationpivot?: Point- Initial pivotonBoundsChange?: (bounds: Bounds) => voidonRotationChange?: (angle: number) => voidonPivotChange?: (pivot: Point) => voidsnapAngles?: number[]- Rotation snap anglessnapZoneRatio?: number- Snap sensitivitymaintainAspectRatio?: boolean- Lock aspect during resizeminWidth?: number- Minimum width constraintminHeight?: number- Minimum height constraint
Returns (UseTransformGizmoReturn):
state: TransformGizmoState- Current transform statehandlers: TransformGizmoHandlers- Event handlersisDragging: boolean- Move operation activeisRotating: boolean- Rotation operation activeisResizing: 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
- @cloudillo/react - React integration hooks
- @cloudillo/types - Type definitions