1.0.2 • Published 8 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