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.1.1

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 表示“请选择”空态。 | | 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,

  // 当前绘图状态(来自 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,

  // 写入(自动入 history)
  setShapeId:     (id: ShapeId | null) => void,
  setTranslation: (t: TranslationState) => void,
  setReflection:  (r: ReflectionState)  => void,
  setRotation:    (r: RotationState)    => 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           = 'shape1' | 'shape2' | 'shape3' | 'shape4' | 'shape5' | 'shape6'
type ReflectionAxis    = 'vertical' | 'horizontal'
type RotationDirection = 'clockwise' | 'counter-clockwise'
type RotationAngle     = '90' | '180' | '270' | '360'

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 }

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

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


内置图形

import { SHAPES, SHAPE_LABELS } from '@dcg-overseas/graphic-transform'

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

SHAPES[id].rotationCenter 取图形的“左下顶点”作为旋转中心(对称图形如菱形取最左点)。SHAPE_LABELS[id] 提供中文显示名,可直接用于 <select>


变换数学

平移

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

反射

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

旋转

rotationCenter 旋转 angle 度,顺时针为视觉负向(SVG y 轴在显示前已通过 CSS scale(1, -1) 翻转)。

const sign = direction === 'clockwise' ? -1 : 1
const rad  = sign * angleDeg * Math.PI / 180
// 标准旋转矩阵 [cos -sin; sin cos]

360° 即恒等变换,270° 等同反方向 90°


主题 (GraphicTransformTheme)

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

{
  axisStrokeWidth?:     number  // 默认 0.07
  gridStrokeWidth?:     number  // 默认 0.04
  axisColor?:           string  // 默认 '#94a3b8'
  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  // 默认 '#ef4444'
  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',
}

<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: #ef4444; }
.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 —— 改任意参数
  • 画布上拖拽图形(松手时一次)
  • palette → canvas 落点(命中时一次)

不进 history:

  • 切换 transformType(消费者状态,与 history 解耦)
  • 拖拽过程中的中间帧

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 { SHAPES, SHAPE_LABELS, GRID_SIZE, HALF_GRID, INITIAL_STATE }

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