npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dcg-overseas/graphic-transform

v0.2.0

Published

Graphic transform tool

Readme

@dcg-overseas/graphic-transform

为小学数学教学场景提供的图形变换工具:在 14×14 网格上对图形执行 平移 / 反射 / 旋转,配套受控状态、内部历史栈、画布拖拽和 palette → canvas 拖放。

包提供 画布 + 操作逻辑;消费者负责 变换类型选择器、参数面板、形状选择器、撤销/重置工具栏。所有 UI 由应用层组合,包不携带 CSS。


安装

pnpm add @dcg-overseas/graphic-transform

reactreact-dom>=18)作为 peer 依赖。


快速开始

import { useState } from 'react'
import {
  GraphicTransformProvider,
  GraphicTransformBoard,
  useGraphicTransformContext,
} from '@dcg-overseas/graphic-transform'
import type { TransformType } from '@dcg-overseas/graphic-transform'

export default function Demo() {
  const [transformType, setTransformType] = useState<TransformType | null>(null)

  return (
    <GraphicTransformProvider transformType={transformType}>
      <ModeSelect value={transformType} onChange={setTransformType} />

      {/* Board fills its parent — wrap it in a sized container */}
      <div style={{ width: 480, height: 480 }}>
        <GraphicTransformBoard />
      </div>

      <Toolbar />
    </GraphicTransformProvider>
  )
}

function ModeSelect({ value, onChange }: {
  value: TransformType | null
  onChange: (t: TransformType | null) => void
}) {
  return (
    <select value={value ?? ''} onChange={(e) => onChange((e.target.value || null) as TransformType | null)}>
      <option value="">请选择</option>
      <option value="translation">平移</option>
      <option value="reflection">反射</option>
      <option value="rotation">旋转</option>
    </select>
  )
}

function Toolbar() {
  const { canUndo, undo, reset } = useGraphicTransformContext()
  return (
    <div>
      <button onClick={undo} disabled={!canUndo}>撤销</button>
      <button onClick={reset}>重置</button>
    </div>
  )
}

完整可运行示例见 apps/web/src/pages/graphic-transform/


设计

GraphicTransformProvider ─── 历史栈 (DrawingState[])
       │                     变换数学
       │                     拖拽/落点 client→math 换算
       │                     theme + classNames 默认值
       │
       ├── GraphicTransformBoard         ← 渲染 SVG 画布
       │
       └── useGraphicTransformContext()  ← 任意后代读取状态、调操作
  • transformType 由消费者受控(useState + select)—— 包不会单独存它,所以切换变换类型不会污染 history 栈。
  • 绘图状态shapeId / shapeOffset / translation / reflection / rotation由包内拥有,每次变更都进 history。撤销永远只回退“一次有意义的绘制操作”。
  • 画布拖拽期间使用 tempOffset 临时显示,松手时一次性 push 到 history。
  • palette → canvas 拖放由消费者实现(自定义预览/ghost),调用 ctx.dropShapeAtClientPoint(shapeId, clientX, clientY) 即可完成落点换算并入 history。

API 参考

<GraphicTransformProvider> props

| Prop | 类型 | 必填 | 说明 | |---|---|---|---| | transformType | TransformType \| null | ✓ | 当前变换类型,消费者受控null 表示“请选择”空态。 | | shapes | Record<ShapeId, ShapeDef> | ✓ | 形状注册表。可用 makeShape() 构建,或直接传字面量;DEFAULT_SHAPES 提供内置 6 图形。 | | shapeIds | ShapeId[] | | palette 下拉的稳定顺序;缺省用 Object.keys(shapes)。 | | initialDrawingState | Partial<DrawingState> | | 仅在首次挂载时使用的初始绘图状态。 | | theme | GraphicTransformTheme | | inline-style fallback 颜色/线宽,见 主题。 | | classNames | GraphicTransformClassNames | | SVG 元素的 className 覆盖,见 classNames 定制。 | | children | ReactNode | ✓ | 任意放在 context 内的组件(Board + 你的工具栏 / 面板)。 |

Provider 没有 width / height<GraphicTransformBoard> 自动填满父容器,父容器 CSS 决定画布尺寸。

<GraphicTransformBoard> props

无。组件从 context 读取所有状态,填满最近的父容器

<div style={{ width: 480, height: 480 }}>
  <GraphicTransformBoard />
</div>

SVG 内已设置 touch-action: none,移动端拖拽不会被浏览器误判为滚动。

useGraphicTransformContext() 返回值

{
  // 受控配置(来自 props)
  transformType: TransformType | null,

  // 形状注册表
  shapes:   Record<ShapeId, ShapeDef>,
  shapeIds: ShapeId[],

  // 当前绘图状态(来自 history 栈顶;拖拽中是 tempOffset)
  shapeId:     ShapeId | null,
  shapeOffset: Point,
  translation: TranslationState,
  reflection:  ReflectionState,
  rotation:    RotationState,

  // 派生(memo)
  currentShape:       ShapeDef | null,
  originalPoints:     Point[],       // 原始多边形(已加 offset)
  transformedPoints:  Point[],       // 变换后多边形(参数不全时为 [])
  maxTranslation:     { up, down, left, right },
  isReflectionActive: boolean,
  isDragging:         boolean,
  rotationCenterCanvas: Point | null, // 生效旋转中心(canvas 坐标交叉点;未选完为 null)

  // 写入(自动入 history)
  setShapeId:     (id: ShapeId | null) => void,
  setTranslation: (t: TranslationState) => void,
  setReflection:  (r: ReflectionState)  => void,
  setRotation:    (r: RotationState)    => void,
  /** 切换旋转中心模式('grid-center' | 'custom' | null);会清空方向/角度/已选交叉点 */
  setRotationCenterMode:   (mode: RotationCenterMode | null) => void,
  /** 选定 canvas 交叉点为旋转中心(custom 模式下 Board 内部调用;会清空方向/角度) */
  setRotationCustomCenter: (p: Point) => void,

  // 历史
  canUndo: boolean,
  undo:    () => void,
  reset:   () => void,

  // 拖放(应用层 palette 调用)
  /** 把图形落到屏幕坐标点;命中画布返回 true 并入 history,否则返回 false */
  dropShapeAtClientPoint: (shapeId: ShapeId, clientX: number, clientY: number) => boolean,

  // theme / classNames(已填默认值)
  theme:      Required<GraphicTransformTheme>,
  classNames: Required<GraphicTransformClassNames>,

  // 内部 — Board 使用,消费者通常不需要
  beginCanvasDrag, updateCanvasDrag, endCanvasDrag, registerSvgRef,
}

数据模型

type TransformType      = 'translation' | 'reflection' | 'rotation'
type ShapeId            = string                 // 开放 id,消费者自建注册表
type ReflectionAxis     = 'vertical' | 'horizontal'
type RotationDirection  = 'clockwise' | 'counter-clockwise'
type RotationAngle      = '90' | '180' | '270' | '360'
type RotationCenterMode = 'grid-center' | 'custom'

interface Point { x: number; y: number }

interface TranslationState {
  upDownVal:    number
  upDownDir:    'up' | 'down'
  leftRightVal: number
  leftRightDir: 'left' | 'right'
}

interface ReflectionState { axis: ReflectionAxis | null }
interface RotationState   {
  direction:    RotationDirection | null
  angle:        RotationAngle | null
  /** 旋转中心模式;null = 未选择(不产生旋转结果) */
  centerMode:   RotationCenterMode | null
  /** 用户点选的网格交叉点(canvas 坐标整数);仅 centerMode === 'custom' 时有意义 */
  customCenter: Point | null
}

interface DrawingState {
  shapeId:     ShapeId | null
  shapeOffset: Point
  translation: TranslationState
  reflection:  ReflectionState
  rotation:    RotationState
}

所有坐标都是网格单位(整数)。grid 范围固定 [-7, 7],即 14×14 格。


内置图形

import { DEFAULT_SHAPES } from '@dcg-overseas/graphic-transform'

| ID | 图形 | 顶点数 | |---|---|---| | shape1 | 直角三角形 | 3 | | shape2 | 凹五边形(M 形) | 5 | | shape3 | 箭头六边形 | 6 | | shape4 | 五边形房屋 | 5 | | shape5 | 蝴蝶结 | 6 | | shape6 | 等腰三角形 | 3 |

DEFAULT_SHAPES[id].label 提供中文显示名,可直接用于 <select>ShapeDef.rotationCenter 字段已 @deprecated(旋转中心现锚定在画布上,见下节),运行时不再读取。


变换数学

平移

(x, y) → (x + dx, y + dy),其中 dx = leftRightDir==='right' ? +val : -valdy 同理(“上”为正)。

反射

  • 垂直对称(纵轴 y 轴):(x, y) → (-x, y)
  • 水平对称(横轴 x 轴):(x, y) → (x, -y)

旋转

画布锚定的旋转中心旋转 angle 度,顺时针为视觉负向(SVG y 轴在显示前已通过 <g transform="scale(1,-1)"> 翻转)。90°/180°/270°/360° 走精确整数路径(坐标交换,无 cos/sin 浮点误差);360° 即恒等变换,270° 等同反方向 90°

旋转中心两种模式rotation.centerMode):

  • 'grid-center'(居中):中心 = 画布原点 (0, 0)。正方形画布绕自身中心旋转 90° 倍数不变,所以此模式下 8 种方向×角度组合恒合法。
  • 'custom'(自行选择):中心 = 用户在画布上点选的任意网格交叉点(15×15 个,可在图形内/外/顶点上)。Board 内置交互:鼠标悬停时最近交叉点显示浅橙预览点(触屏为按下即预览),点击(按下→抬起位移 < 5px,与拖图消歧)后确定,橙色圆点落在该点并调用 setRotationCustomCenter

未选模式(或 custom 模式未点选交叉点)时 rotationCenterCanvasnull,不产生旋转结果,消费者应禁用方向/角度选项。切换模式或换点会清空方向/角度(旧结果绕另一点算出)并入历史。

拖图钳制:中心锚在画布上,拖动图形时中心不动,图形移动 Δo 时旋转像移动 R(Δo)。包内用盒交集精确钳制——合法 offset 区域 = B_base ∩ R⁻¹(B_image)(90° 倍数旋转下轴对齐盒保持轴对齐),保证拖到任何位置原图与像都不出界。rotationLegality 随中心/位置自动重算。


主题 (GraphicTransformTheme)

inline-style fallback,仅在对应 className 为空时生效。如果你给 classNames 全填了,theme 颜色不会有任何效果(因为 className 模式完全交给 CSS)。

{
  axisStrokeWidth?:     number  // 默认 0.02
  gridStrokeWidth?:     number  // 默认 0.02
  axisColor?:           string  // 默认 '#e2e8f0'
  gridColor?:           string  // 默认 '#e2e8f0'
  originalFill?:        string  // 默认 '#bfdbfe'   原始图形
  originalStroke?:      string  // 默认 '#2563eb'
  transformedFill?:     string  // 默认 '#fecaca'   变换后(平移/旋转)
  transformedStroke?:   string  // 默认 '#dc2626'
  reflectionFill?:      string  // 默认 '#fed7aa'   反射时覆盖 transformed*
  reflectionStroke?:    string  // 默认 '#ea580c'
  mirrorLineColor?:     string  // 默认 '#ef4444'
  rotationCenterColor?:        string  // 默认 '#f97316'  生效旋转中心(橙)
  rotationCenterPreviewColor?: string  // 默认 '#fdba74'  悬停预览点(浅橙)
  boundaryColor?:       string  // 默认 '#e2e8f0'   画布内边界
  boundaryStrokeWidth?: number  // 默认 0.05
}

适合“零 CSS 即可使用”的场景。下一节是更灵活的 className 定制。


classNames 定制

包不携带 CSS。如果你需要伪类(:hover.dragging)、媒体查询、CSS 变量,请走 className 路径:

import type { GraphicTransformClassNames } from '@dcg-overseas/graphic-transform'

const BOARD_CLASS_NAMES: GraphicTransformClassNames = {
  svg:                          'gt-board-svg',
  boundary:                     'gt-boundary',
  gridLine:                     'gt-grid-line',
  axis:                         'gt-axis',
  mirrorLine:                   'gt-mirror-line',
  shapeOriginal:                'gt-shape-original',
  shapeOriginalDragging:        'dragging',           // 拖拽时附加在 shapeOriginal 上
  shapeTransformed:             'gt-shape-transformed',
  shapeTransformedReflection:   'reflection',         // 反射时附加在 shapeTransformed 上
  rotationCenter:               'gt-rotation-center',
  rotationCenterPreview:        'gt-rotation-center-preview',
}

<GraphicTransformProvider transformType={transformType} classNames={BOARD_CLASS_NAMES}>
  …
</GraphicTransformProvider>

对应 CSS 示例:

.gt-board-svg            { width: 100%; height: 100%; touch-action: none; }
.gt-boundary             { fill: none; stroke: #e2e8f0; stroke-width: 0.05; }
.gt-grid-line            { stroke: #e2e8f0; }
.gt-axis                 { stroke: #94a3b8; }
.gt-mirror-line          { stroke: #ef4444; }
.gt-rotation-center      { fill: #f97316; }
.gt-rotation-center-preview { fill: #fdba74; }
.gt-shape-original       { fill: #bfdbfe; stroke: #2563eb; cursor: grab; stroke-width: 0.1; }
.gt-shape-original.dragging       { cursor: grabbing; }
.gt-shape-transformed    { fill: #fecaca; stroke: #dc2626; fill-opacity: 0.7; pointer-events: none; }
.gt-shape-transformed.reflection  { fill: #fed7aa; stroke: #ea580c; }

取舍:对每个 SVG 元素,如果你给了 className,Board 不再写 inline stylefill/stroke 都靠你的 CSS);如果某个 className 是空字符串,则使用 theme 的 inline-style fallback。可以混用 —— 比如只给 shapeOriginal 一个 className(为了 :hover),其它元素继续吃 theme。


palette → canvas 拖放

包不渲染 palette,但提供了“落点换算”原语:

function MyShapePalette() {
  const { dropShapeAtClientPoint } = useGraphicTransformContext()
  const [drag, setDrag] = useState<{ shapeId: ShapeId; x: number; y: number } | null>(null)

  return (
    <div
      style={{ touchAction: 'none', cursor: 'grab' }}
      onPointerDown={(e) => {
        e.currentTarget.setPointerCapture(e.pointerId)
        setDrag({ shapeId: 'shape1', x: e.clientX, y: e.clientY })
      }}
      onPointerMove={(e) => drag && setDrag({ ...drag, x: e.clientX, y: e.clientY })}
      onPointerUp={(e) => {
        if (!drag) return
        e.currentTarget.releasePointerCapture(e.pointerId)
        dropShapeAtClientPoint(drag.shapeId, e.clientX, e.clientY)
        setDrag(null)
      }}
      onPointerCancel={() => setDrag(null)}
    >
      {/* 你的预览 + 跟随手指的 ghost */}
    </div>
  )
}

dropShapeAtClientPoint 会:

  1. 检查屏幕坐标是否落在 SVG 边界内,否则返回 false 不入 history
  2. 通过 getScreenCTM()clientX/Y 转为 SVG 网格坐标
  3. Math.round 吸附到整数格
  4. 用形状的多边形顶点裁剪到 ±7 边界
  5. 一次性 push 到 history

完整 palette 示例(含 ghost):apps/web/src/pages/graphic-transform/components.tsx


撤销策略

每次以下操作进 history,可单步撤销:

  • setShapeId —— 选 / 换 / 清图形(保留画布锚定的旋转中心,清空方向/角度)
  • setTranslation / setReflection / setRotation —— 改任意参数
  • setRotationCenterMode —— 切换旋转中心模式(同时清空方向/角度/已选交叉点,一步可撤销)
  • setRotationCustomCenter —— 点选 / 换选交叉点(同时清空方向/角度,一步可撤销)
  • 画布上拖拽图形(松手时一次)
  • palette → canvas 落点(命中时一次)

不进 history:

  • 切换 transformType(消费者状态,与 history 解耦)
  • 拖拽过程中的中间帧
  • 选点模式的悬停预览点(Board 内部瞬态 UI)

reset() 清空整个 history。


网格约定

  • 14×14 格,坐标范围 [-7, 7]HALF_GRID = 7
  • SVG viewBox="-7 -7 14 14",通过 CSS transform: scale(1, -1) 把 y 轴翻转为数学习惯(向上为正)
  • 1 格 = 画布像素 / 14,响应父容器宽度

移动端

  • 默认 touch-action: none,单指拖拽不会触发页面滚动
  • 使用 Pointer Events(统一鼠标 / 触摸 / 笔),setPointerCapture 保证手指移出元素后事件继续触发
  • 已处理 pointercancel:系统中断手势时会清理拖拽状态

导出

// 组件 + Hook
export { GraphicTransformProvider, GraphicTransformBoard }
export { useGraphicTransformContext, GraphicTransformContext }

// 常量
export { DEFAULT_SHAPES, GRID_SIZE, HALF_GRID, INITIAL_STATE }

// 几何工具(纯函数)
export {
  applyTransform, clampDragOffset, clampOffsetToGrid, clientToSvgPoint,
  computeMaxTranslation, computeRotationLegalityMap, EMPTY_ROTATION_LEGALITY,
  isClientPointInsideSvg, makeShape, pointsInBounds, snapToGridIntersection,
}

// 类型
export type {
  AppState, DrawingState, Point, ShapeDef, ShapeId,
  TransformType, TranslationState, ReflectionState, RotationState,
  ReflectionAxis, RotationAngle, RotationCenterMode, RotationDirection,
  RotationLegalityMap,
  GraphicTransformTheme, GraphicTransformClassNames,
  GraphicTransformProviderProps, GraphicTransformContextValue,
}