1.0.2 • Published 5 months ago
@picpin/core v1.0.2
@picpin/core
What is PicPin?
PicPin (Picture Pin) is a headless React library that provides all the logic needed to create interactive image viewers with:
- 🔍 Zoom - Mouse wheel, touch gestures, and programmatic controls
 - 🖐️ Pan - Drag to move images when zoomed
 - 📍 Pins - Add markers/annotations at specific coordinates
 - 🎯 Coordinates - Automatic conversion between image, viewport, and container coordinates
 - 🎨 Headless - No forced styles, you have complete control over the UI
 - ⚡ Performant - Optimized calculations and minimal re-renders
 - 📦 Lightweight - ~15kb gzipped with only one dependency (zustand)
 - 🔧 TypeScript - Fully typed for excellent DX
 
Installation
npm install @picpin/core
# or
yarn add @picpin/core
# or
pnpm add @picpin/coreQuick Start
import {
  PicViewerProvider,
  ViewerContainer,
  ViewerImage,
  PinLayer,
  usePicPin,
  useViewPort,
  useZoom,
  useDynamicPins
} from "@picpin/core";
function App() {
  // Create a client service (your custom logic)
  const clientService = (context) => ({
    onPinClick: (pin) => console.log("Pin clicked:", pin)
  });
  return (
    <PicViewerProvider picPinClientService={clientService}>
      <ImageViewer />
    </PicViewerProvider>
  );
}
function ImageViewer() {
  const { handlers, containerRef, imageRef, controller } = usePicPin();
  const viewport = useViewPort();
  const zoom = useZoom();
  const dynamicPins = useDynamicPins();
  // Load an image
  React.useEffect(() => {
    controller.loadFile({
      url: "https://picsum.photos/800/600",
      name: "sample.jpg"
    });
  }, []);
  // Add pin on click
  const handleClick = (e) => {
    const rect = containerRef.current?.getBoundingClientRect();
    if (!rect) return;
    
    const coords = controller.getImageCoordinates(
      e.clientX - rect.left,
      e.clientY - rect.top
    );
    
    if (coords) {
      controller.addPin(coords.x, coords.y);
    }
  };
  return (
    <ViewerContainer
      containerRef={containerRef}
      handlers={{
        ...handlers.containerProps(),
        onClick: handleClick
      }}
      style={{ width: "100%", height: "500px", position: "relative" }}
    >
      <ViewerImage
        imageRef={imageRef}
        src="https://picsum.photos/800/600"
        viewport={viewport}
        zoom={zoom}
      />
      
      <PinLayer viewport={viewport}>
        {dynamicPins.map(({ pin, style }) => (
          <div key={pin.id} style={style}>
            📍
          </div>
        ))}
      </PinLayer>
    </ViewerContainer>
  );
}Use Cases
PicPin is perfect for building:
- 🏠 Virtual Tours - Navigate between rooms with hotspot navigation
 - 🎨 Color Pickers - Pick multiple colors from images
 - 📝 Image Annotators - Add comments and notes to specific areas
 - 🗺️ Interactive Maps - Navigate large images with markers
 - 🏥 Medical Viewers - Mark points of interest in medical images
 - 🛍️ Product Showcases - Highlight product features
 - 📐 Measurement Tools - Measure distances and areas
 - 🎮 Game Maps - Interactive game world maps
 
Core Concepts
1. Headless Architecture
PicPin provides the logic, you provide the UI. This means:
- Complete control over styling
 - Use any UI library (Material-UI, Ant Design, Tailwind, etc.)
 - No CSS conflicts
 - Smaller bundle size
 
2. Coordinate Systems
PicPin handles three coordinate systems automatically:
- Image coordinates - Relative to the original image
 - Viewport coordinates - Relative to the visible area
 - Container coordinates - Relative to the DOM container
 
3. Pin Agnostic
Pins are just data with positions. You decide:
- What they look like
 - What they do when clicked
 - What metadata they carry
 - How they behave
 
API Overview
Hooks
// Main hook - access everything
const { controller, handlers, containerRef, imageRef } = usePicPin();
// State hooks with selectors
const pins = usePicPinState(state => state.pins);
const viewport = useViewPort();
const zoom = useZoom();
// Pins with calculated styles
const dynamicPins = useDynamicPins();
// Keyboard shortcuts
useKeyboardShortcuts({
  onZoomIn: () => controller.zoomIn(),
  onZoomOut: () => controller.zoomOut(),
  onDelete: () => controller.deleteSelectedPin()
});Controller API
// File operations
controller.loadFile({ url, name });
controller.clearFile();
// Pin operations
controller.addPin(x, y, metadata);
controller.removePin(pinId);
controller.selectPin(pinId);
controller.setPins(pins);
// View operations
controller.zoomIn();
controller.zoomOut();
controller.zoomTo(level);
controller.fitToView();
controller.resetView();
// Coordinate conversion
controller.getImageCoordinates(containerX, containerY);
controller.getContainerCoordinates(imageX, imageY);
// Import/Export
controller.exportView();
controller.importView(viewData);Components
All components are headless (no styles):
<ViewerContainer>      {/* Main container */}
<ViewerImage>         {/* Image with transforms */}
<PinLayer>           {/* Pin container with viewport offset */}
<PinWrapper>         {/* Individual pin wrapper */}
<DefaultPin>         {/* Example pin component */}Advanced Examples
Photo Navigator with Hotspots
const photos = [
  {
    id: "room1",
    url: "/room1.jpg",
    hotspots: [
      { position: { x: 400, y: 300 }, targetRoom: "room2" }
    ]
  }
];
function PhotoNavigator() {
  const [currentRoom, setCurrentRoom] = useState("room1");
  
  const clientService = (context) => ({
    onPinClick: (pin) => {
      setCurrentRoom(pin.metadata.targetRoom);
    }
  });
  
  // ... render logic
}Multi-point Color Picker
function ColorPicker() {
  const [colors, setColors] = useState([]);
  
  const clientService = (context) => ({
    onPinAdd: async (pin) => {
      const color = await extractColorAtPoint(
        context.imageRef.current,
        pin.position
      );
      setColors([...colors, { id: pin.id, color }]);
    }
  });
  
  // ... render logic
}Features
- ✅ Touch Support - Works on mobile devices
 - ✅ Keyboard Shortcuts - Customizable keyboard controls
 - ✅ Events System - React to viewer changes
 - ✅ Constraints - Limit zoom and pan ranges
 - ✅ High DPI Support - Sharp images on retina displays
 - ✅ Server-Side Rendering - SSR compatible
 - ✅ Accessibility - Keyboard navigation support
 
Browser Support
- Chrome (latest)
 - Firefox (latest)
 - Safari (latest)
 - Edge (latest)
 - iOS Safari (latest)
 - Chrome Android (latest)
 
TypeScript
PicPin is written in TypeScript and provides comprehensive type definitions:
import { Pin, ViewportState, PicViewerController } from "@picpin/core";
interface MyPinMetadata {
  label: string;
  color: string;
  category: "hotspot" | "annotation" | "measurement";
}
const pin: Pin<MyPinMetadata> = {
  id: "pin-1",
  position: { x: 100, y: 200 },
  metadata: {
    label: "Room entrance",
    color: "#ff0000",
    category: "hotspot"
  },
  createdAt: new Date(),
  updatedAt: new Date()
};Performance
PicPin is optimized for performance:
- Efficient State Management - Uses Zustand with selectors
 - Minimal Re-renders - Components only update when needed
 - CSS Transforms - Hardware-accelerated positioning
 - Event Delegation - Single event listener for all pins
 - Memoized Calculations - Complex calculations are cached
 
Documentation
Full documentation is available at GitHub:
Contributing
Contributions are welcome! Please read our Contributing Guide for details.
License
MIT © Your Name