1.0.2 • Published 5 months ago

@picpin/core v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

@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/core

Quick 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


1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago