image-annotation-usage

Use when integrating @frank17008/image-annotation React component into web applications, including basic setup, controlled/uncontrolled mode, custom image upload, styling, or debugging annotation rendering issues, canvas blank problems, or annotations not updating.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "image-annotation-usage" with this command: npx skills add frank17008/image-annotation-usage

Image Annotation Component Usage

Overview

@frank17008/image-annotation is a React + TypeScript image annotation component with Canvas 2D. Supports rectangle, circle, arrow, text, and freehand tools with undo/redo, drag, export, keyboard shortcuts, zoom/pan, and DPR-aware rendering.

Package location: packages/image-annotation/src/ Published path: @frank17008/image-annotation

When to Use

Trigger phrases:

  • "How to integrate image annotation component"
  • "Add annotation to my React app"
  • "Canvas blank even though image loads"
  • "Annotations not saving"
  • "Custom image upload with annotation"
  • "Controlled mode not working"
  • "HiDPI/Retina display blurry"

Installation

pnpm add @frank17008/image-annotation
# or
npm install @frank17008/image-annotation

Basic Integration

Minimal Setup

import { useState } from 'react';
import { ImageAnnotation, type Annotation } from '@frank17008/image-annotation';
import '@frank17008/image-annotation/dist/index.css';

export default function App() {
  const [annotations, setAnnotations] = useState<Annotation[]>([]);

  return (
    <div style={{ height: 600 }}>
      <ImageAnnotation
        src="https://example.com/image.jpg"
        onChange={setAnnotations}
      />
    </div>
  );
}

Critical: Parent container MUST have explicit height. Without it, canvas has 0×0 dimensions → blank canvas.

Type Import

Always use explicit type import for TypeScript:

import type { Annotation, ToolType } from '@frank17008/image-annotation';
// or
import { type Annotation, type ToolType } from '@frank17008/image-annotation';

Controlled vs Uncontrolled Mode

Uncontrolled (Default)

<ImageAnnotation
  src="image.jpg"
  onChange={(annotations) => console.log(annotations)}
/>

The component manages its own state internally.

Controlled (External State)

const [myAnnotations, setMyAnnotations] = useState<Annotation[]>([]);

<ImageAnnotation
  src="image.jpg"
  value={myAnnotations}
  onChange={setMyAnnotations}
/>

Rules:

  1. value → external annotations (read-only for component)
  2. onChange → called when annotations change
  3. If both value and onChange provided → controlled mode
  4. If neither → uncontrolled mode
  5. If only value without onChange → React warning

Sync Controlled to Uncontrolled

<ImageAnnotation
  src={src}
  value={annotations}
  onChange={(newAnnotations) => {
    setAnnotations(newAnnotations);
    // Save to backend, localStorage, etc.
    saveToBackend(newAnnotations);
  }}
/>

Custom Image Upload

Pattern 1: External Button

import { useState, useRef } from 'react';
import { ImageAnnotation } from '@frank17008/image-annotation';

export function ImageUploader() {
  const [imageSrc, setImageSrc] = useState<string | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const result = event.target?.result;
      if (typeof result === 'string') {
        setImageSrc(result); // base64 string
      }
    };
    reader.readAsDataURL(file);
    e.target.value = ''; // reset for same file re-select
  };

  return (
    <div style={{ height: 600 }}>
      <input
        ref={fileInputRef}
        type="file"
        accept="image/*"
        onChange={handleFileChange}
        style={{ display: 'none' }}
      />
      <button onClick={() => fileInputRef.current?.click()}>
        Upload Image
      </button>
      {imageSrc && (
        <ImageAnnotation
          src={imageSrc}
          onChange={() => {}}
        />
      )}
    </div>
  );
}

Pattern 2: Component's Upload Button

const handleUpload = () => fileInputRef.current?.click();

<ImageAnnotation
  src={imageSrc}
  onChange={setAnnotations}
  onUpload={handleUpload}
/>

The component displays an upload button in toolbar when onUpload is provided.

Annotation Types Reference

// Rectangle annotation
{ type: 'rectangle', x: number, y: number, width: number, height: number, color: string, lineWidth?: number }

// Circle annotation
{ type: 'circle', x: number, y: number, radius: number, color: string, lineWidth?: number }

// Arrow annotation
{ type: 'arrow', x: number, y: number, toX: number, toY: number, color: string, lineWidth?: number }

// Text annotation
{ type: 'text', x: number, y: number, text: string, color: string, lineWidth?: number }

// Freehand annotation
{ type: 'freehand', points: Array<{ x: number, y: number }>, color: string, lineWidth?: number }

Type Export

import type {
  Annotation,
  RectangleAnnotation,
  CircleAnnotation,
  ArrowAnnotation,
  TextAnnotation,
  FreehandAnnotation,
  ToolType,
  Point,
} from '@frank17008/image-annotation';

Props API

PropTypeRequiredDescription
srcstringImage URL or base64
valueAnnotation[]Controlled annotations
onChange(annotations: Annotation[]) => voidAnnotation change callback
classNamestringContainer className
onUpload() => voidUpload button callback

Zoom and Pan

Programmatic Zoom

const handleZoomIn = () => {
  // Component doesn't expose zoom API directly
  // Use keyboard shortcuts: Ctrl++
};

// Or via keyboard shortcuts (user must press):
// - Ctrl++ : Zoom in
// - Ctrl+- : Zoom out
// - Ctrl+0 : Reset zoom

Space + Drag Pan

Hold Space + drag to pan the canvas.

Zoom Ratio Display

The toolbar shows current zoom percentage (e.g., "100%", "150%").

Exporting Annotations

On Change Callback

<ImageAnnotation
  src={src}
  onChange={(annotations) => {
    // Save to database
    await saveAnnotations(imageId, annotations);

    // Or export as JSON
    const json = JSON.stringify(annotations, null, 2);
    download(json, 'annotations.json', 'application/json');
  }}
/>

Export Image with Annotations

Use the toolbar's export button, or programmatically:

import { download } from '@frank17008/image-annotation';
// Not currently exported - use toolbar button

Keyboard Shortcuts

ShortcutAction
DeleteDelete selected annotation
EscapeCancel text input
Ctrl+ZUndo
Ctrl+Y / Ctrl+Shift+ZRedo
Ctrl++ / Ctrl+=Zoom in
Ctrl+-Zoom out
Ctrl+0Reset zoom
Space + DragPan canvas

Styling Customization

CSS Variables

:root {
  --annotation-primary: #FF0000;
  --annotation-toolbar-bg: #FFFFFF;
}

Container Styling

<ImageAnnotation
  src={src}
  className="my-annotation-container"
/>
.my-annotation-container {
  border: 1px solid #ddd;
  border-radius: 8px;
}

Debugging Common Issues

Issue 1: Canvas Completely Blank

Symptoms:

  • Image loads successfully (network request completes)
  • Canvas area is empty/white

Root Causes and Solutions:

CauseDiagnosisFix
Parent has no heightContainer has height: auto or no height setSet explicit height: style={{ height: 600 }}
DPR mismatchRetina/HiDPI display, canvas looks tinyComponent handles DPR automatically
Image load errorCheck browser console for CORS errorsAdd crossOrigin="anonymous" to server
Zero canvas sizeCheck canvas element dimensionsEnsure parent has explicit dimensions

Issue 2: Annotations Not Saving

Symptoms:

  • Draw annotations but array remains empty
  • onChange callback never fires

Root Causes and Solutions:

CauseDiagnosisFix
Stale stateUsing prev => prev incorrectlyPass new array directly: setAnnotations(newValue)
Missing onChangeNo callback providedAdd onChange={setAnnotations}
Controlled conflictBoth value and internal stateProvide onChange with value

Correct pattern:

// WRONG - stale closure
onChange={(annotations) => {
  setAnnotations((prev) => [...prev, ...annotations]);
}}

// CORRECT - direct assignment
onChange={setAnnotations}

Issue 3: Blurry on HiDPI/Retina Displays

Diagnosis:

  • Canvas appears blurry or pixelated
  • Only happens on Retina displays

Root Cause: Missing devicePixelRatio scaling

Fix: The component handles DPR automatically since v1.x. Ensure:

  1. CSS import is included
  2. Parent container has proper dimensions

Issue 4: High-DPI Canvas Blank

Symptoms:

  • Works on standard displays
  • Blank on Retina/HiDPI

Root Cause: Canvas dimensions not scaled by devicePixelRatio

Diagnosis:

// In browser console
const canvas = document.querySelector('canvas');
console.log('Canvas size:', canvas.width, canvas.height);
console.log('Display size:', canvas.style.width, canvas.style.height);
console.log('DPR:', window.devicePixelRatio);

If canvas.width === canvas.style.width (not multiplied by DPR), this is the bug.

Issue 5: Text Annotation Not Updating

Symptoms:

  • Text tool selected
  • Click to add text
  • Text doesn't appear or doesn't save

Solution: Press Enter or click outside to commit text input.

Issue 6: Undo/Redo Not Working

Symptoms:

  • Press Ctrl+Z but nothing happens

Root Cause: No history to undo (first annotation can't be undone)

Issue 7: SSR/Next.js Issues

Symptoms:

  • "window is not defined"
  • Canvas only renders client-side

Solution:

'use client';
import dynamic from 'next/dynamic';

const ImageAnnotation = dynamic(
  () => import('@frank17008/image-annotation').then((mod) => mod.default),
  { ssr: false }
);

Build Commands

pnpm build:lib      # Build package to dist/
pnpm dev:lib       # Watch mode for library
pnpm start         # Run demo app
pnpm build:all    # Build everything

Dependencies

  • React 18+
  • TypeScript 4.5+
  • Canvas 2D API

Common Mistakes

❌ Forgetting CSS Import

// MISSING - component won't render correctly
import { ImageAnnotation } from '@frank17008/image-annotation';

// REQUIRED
import '@frank17008/image-annotation/dist/index.css';

❌ Parent Without Height

// WRONG - canvas has 0x0
<div><ImageAnnotation src="..." /></div>

// CORRECT - explicit height
<div style={{ height: 600 }}><ImageAnnotation src="..." /></div>

❌ Wrong Type Import

// WRONG
import { Annotation } from '@frank17008/image-annotation';

// CORRECT
import type { Annotation } from '@frank17008/image-annotation';

❌ Not Linking onChange with value

// WRONG - controlled without onChange (React warning)
<ImageAnnotation src="..." value={annotations} />

// CORRECT - controlled mode
<ImageAnnotation
  src="..."
  value={annotations}
  onChange={setAnnotations}
/>

Performance Tips

Large Images

For images > 5MB:

  1. Resize before passing to component
  2. Use appropriate container dimensions
  3. Consider lazy loading

Many Annotations (>100)

The component redraws on each change. For performance:

  1. Reduce lineWidth
  2. Simplify freehand paths (reduce point count)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

evens-结算

AI驱动的智能结算助手,支持话题词精准识别、自然语言规则解析、规则确认流程、多种结算模式(达标瓜分/排名赛/混合模式),数据本地处理保障安全。触发词:「一组结算」「帮我结算」「请结算」「结算活动」。

Registry SourceRecently Updated
General

Model Switch

OpenClaw 一键切换AI模型技能。懒人触发词:切到xxx、当前模型、模型问题、添加/移除模型、模型对比/列表。解决"切换模型后为什么总是失败"的痛点。

Registry SourceRecently Updated
General

飞书文档翻译助手

飞书文档翻译助手 — 在飞书文档之间进行中英文互译,支持全文翻译、段落翻译、双语对照。当用户需要翻译飞书文档、英文文档转中文、中文文档转英文、或创建双语版本文档时使用。触发词:翻译飞书文档、文档翻译、中英互译、双语文档、translate feishu doc、飞书英文。

Registry SourceRecently Updated
General

Stock Terminal

Provides Bloomberg-style synthesized stock and market reports via typed commands like open, compare, daily brief, mood, screen smart-money, flow, and news, a...

Registry SourceRecently Updated