HextaUI

BetaVercel UI SDK 0.1.0-beta.1 üzerine kurulu. API’si değişebilir.

Image cropper

Vercel UI SDK üzerinde yeniden boyutlandırılabilir seçim, en-boy oranları, yuvarlak avatar çerçevesi, yakınlaştırma, döndürme ve çevirme sunan bir görsel kırpıcı.

  • Bileşen
  • 10 parça
  • 6 örnek
Bağımlılıklar
  • @tabler/icons-react
  • @ui-sdk/core
  • @ui-sdk/react
  • class-variance-authority
A rowing boat on a green mountain lake
"use client"

import * as React from "react"
import { cropImage } from "@ui-sdk/react/image"

import { Button } from "@/components/ui/button"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import {
  ImageCropper,
  ImageCropperGrid,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperSelection,
  ImageCropperViewport,
  type ImageCrop,
} from "@/components/ui/ui-sdk-image-cropper"

const photo =
  "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200&q=80&auto=format&fit=crop"

const ratios = [
  { label: "Free", value: "free", ratio: undefined },
  { label: "Square", value: "square", ratio: 1 },
  { label: "Wide", value: "wide", ratio: 16 / 9 },
]

function UiSdkImageCropperDemo() {
  const [ratio, setRatio] = React.useState("wide")
  const [crop, setCrop] = React.useState<ImageCrop>()
  const [result, setResult] = React.useState<string>()

  React.useEffect(
    () => () => {
      if (result) URL.revokeObjectURL(result)
    },
    [result]
  )

  async function save() {
    if (!crop) return
    const file = await cropImage(photo, crop, { width: 480 })
    setResult(URL.createObjectURL(file))
  }

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <ToggleGroup
        aria-label="Aspect ratio"
        value={[ratio]}
        onValueChange={(next) => {
          if (next[0]) setRatio(next[0])
        }}
        variant="outline"
        size="sm"
      >
        {ratios.map((item) => (
          <ToggleGroupItem key={item.value} value={item.value}>
            {item.label}
          </ToggleGroupItem>
        ))}
      </ToggleGroup>
      <ImageCropper
        aria-label="Photo"
        src={photo}
        aspectRatio={ratios.find((item) => item.value === ratio)?.ratio}
        minWidth={80}
        minHeight={80}
        crop={crop}
        onCropChange={setCrop}
      >
        <ImageCropperViewport>
          <ImageCropperImage alt="A rowing boat on a green mountain lake" />
          <ImageCropperSelection>
            <ImageCropperGrid />
            <ImageCropperHandles />
          </ImageCropperSelection>
        </ImageCropperViewport>
      </ImageCropper>
      <div className="flex items-center gap-3">
        <Button size="sm" onClick={save} disabled={!crop}>
          Crop
        </Button>
        {crop && (
          <span className="text-sm text-muted-foreground tabular-nums">
            {Math.round(crop.width)} × {Math.round(crop.height)}
          </span>
        )}
      </div>
      {result && (
        <img
          src={result}
          alt="The cropped photo"
          className="w-full rounded-lg"
        />
      )}
    </div>
  )
}

export { UiSdkImageCropperDemo }
pnpm dlx shadcn@latest add https://hextaui.com/r/ui-sdk-image-cropper.json

Bileşeni, HextaUI tema token'larını ve bileşenin bağımlı olduğu tüm HextaUI bileşenlerini ekler.

UI SDK, ui-sdk adlı bir cascade katmanında kendi CSS’ini biraz ekler. Sınıflarınızın kazanması için bu katmanı genel stil dosyanızın en üstünde, Tailwind import’unun üzerinde tanımlayın.

@layer ui-sdk;

@import "tailwindcss";
import {
  ImageCropper,
  ImageCropperGrid,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperSelection,
  ImageCropperViewport,
} from "@/components/ui/ui-sdk-image-cropper"
<ImageCropper aria-label="Cover photo" src={src} aspectRatio={3}>
  <ImageCropperViewport>
    <ImageCropperImage alt="Your cover photo" />
    <ImageCropperSelection>
      <ImageCropperGrid />
      <ImageCropperHandles />
    </ImageCropperSelection>
  </ImageCropperViewport>
</ImageCropper>

Kırpma, görselin kendi pikselleri cinsinden, döndürme ve çevirmesiyle birlikte bir kutudur. Yükleyebileceğiniz bir dosya almak için onu cropImage fonksiyonuna verin. Başka bir siteden gelen görsel CORS gerektirir; yoksa tarayıcı sayfanın piksellerini okumasına izin vermez.

import { cropImage } from "@ui-sdk/react/image"

const file = await cropImage(src, crop, { width: 512 })
ImageCropper
├── ImageCropperViewport
│   ├── ImageCropperImage
│   └── ImageCropperSelection
│       ├── ImageCropperGrid
│       └── ImageCropperHandles
├── ImageCropperZoom
├── ImageCropperRotate
└── ImageCropperFlip

mode="frame" ile seçim sabit kalır, altındaki görseli kaydırıp yakınlaştırırsınız. shape="circle" onu yuvarlar; bu yüzden cropImage fonksiyonuna da shape: "circle" verin.

"use client"

import * as React from "react"
import { IconUser } from "@tabler/icons-react"
import { cropImage } from "@ui-sdk/react/image"

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  ImageCropper,
  ImageCropperImage,
  ImageCropperRotate,
  ImageCropperSelection,
  ImageCropperViewport,
  ImageCropperZoom,
  type ImageCrop,
} from "@/components/ui/ui-sdk-image-cropper"

const photo =
  "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1200&q=80&auto=format&fit=crop"

function UiSdkImageCropperAvatar() {
  const [crop, setCrop] = React.useState<ImageCrop>()
  const [avatar, setAvatar] = React.useState<string>()

  React.useEffect(
    () => () => {
      if (avatar) URL.revokeObjectURL(avatar)
    },
    [avatar]
  )

  async function save() {
    if (!crop) return
    const file = await cropImage(photo, crop, { width: 128, shape: "circle" })
    setAvatar(URL.createObjectURL(file))
  }

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <ImageCropper
        aria-label="Profile photo"
        src={photo}
        mode="frame"
        aspectRatio={1}
        shape="circle"
        maxZoom={4}
        crop={crop}
        onCropChange={setCrop}
      >
        <ImageCropperViewport>
          <ImageCropperImage alt="Snowy peaks above a sea of clouds" />
          <ImageCropperSelection />
        </ImageCropperViewport>
        <div className="flex items-center gap-2">
          <ImageCropperZoom />
          <ImageCropperRotate />
        </div>
      </ImageCropper>
      <div className="flex items-center gap-3">
        <Button size="sm" onClick={save} disabled={!crop}>
          Save avatar
        </Button>
        <Avatar>
          {avatar && <AvatarImage src={avatar} alt="Your new avatar" />}
          <AvatarFallback>
            <IconUser />
          </AvatarFallback>
        </Avatar>
      </div>
    </div>
  )
}

export { UiSdkImageCropperAvatar }

ImageCropperRotate görseli saat yönünde çeyrek tur döndürür, ImageCropperFlip aynalar. Alt öğe verilmezse İngilizce etiketli simge düğmeleridir.

import {
  ImageCropper,
  ImageCropperFlip,
  ImageCropperGrid,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperRotate,
  ImageCropperSelection,
  ImageCropperViewport,
} from "@/components/ui/ui-sdk-image-cropper"

const photo =
  "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200&q=80&auto=format&fit=crop"

function UiSdkImageCropperRotateFlip() {
  return (
    <ImageCropper
      aria-label="Photo"
      src={photo}
      aspectRatio={4 / 5}
      className="max-w-sm"
    >
      <ImageCropperViewport>
        <ImageCropperImage alt="A rowing boat on a green mountain lake" />
        <ImageCropperSelection>
          <ImageCropperGrid />
          <ImageCropperHandles />
        </ImageCropperSelection>
      </ImageCropperViewport>
      <div className="flex gap-2">
        <ImageCropperRotate />
        <ImageCropperFlip />
        <ImageCropperFlip direction="vertical" />
      </div>
    </ImageCropper>
  )
}

export { UiSdkImageCropperRotateFlip }

Dosya input’undan gelen bir fotoğrafı object URL ile kırpın. Yeni bir fotoğrafın sıfırdan başlaması için kırpıcıya bir key verin.

"use client"

import * as React from "react"

import { Input } from "@/components/ui/input"
import {
  ImageCropper,
  ImageCropperGrid,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperSelection,
  ImageCropperViewport,
} from "@/components/ui/ui-sdk-image-cropper"

function UiSdkImageCropperUpload() {
  const [src, setSrc] = React.useState<string>()

  React.useEffect(
    () => () => {
      if (src) URL.revokeObjectURL(src)
    },
    [src]
  )

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Input
        type="file"
        accept="image/*"
        aria-label="Choose a photo"
        onChange={(event) => {
          const file = event.target.files?.[0]
          if (file) setSrc(URL.createObjectURL(file))
        }}
      />
      {src ? (
        <ImageCropper key={src} aria-label="Photo" src={src} aspectRatio={1}>
          <ImageCropperViewport>
            <ImageCropperImage alt="The photo you chose" />
            <ImageCropperSelection>
              <ImageCropperGrid />
              <ImageCropperHandles />
            </ImageCropperSelection>
          </ImageCropperViewport>
        </ImageCropper>
      ) : (
        <p className="text-sm text-muted-foreground">
          Choose a photo to crop it.
        </p>
      )}
    </div>
  )
}

export { UiSdkImageCropperUpload }

onCropCommit bir sürükleme veya tuş basışı bittiğinde çalışır; kaydetmeyi orada yapın. createImageCropperController() ile oluşturulan bir controller kırpmayı her yerden ayarlar.

"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  createImageCropperController,
  ImageCropper,
  ImageCropperGrid,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperSelection,
  ImageCropperViewport,
  type ImageCrop,
} from "@/components/ui/ui-sdk-image-cropper"

const photo =
  "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200&q=80&auto=format&fit=crop"

const start = { x: 120, y: 80, width: 480, height: 480 }

function UiSdkImageCropperControlled() {
  const [cropper] = React.useState(createImageCropperController)
  const [saved, setSaved] = React.useState<ImageCrop | null>(null)

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <ImageCropper
        aria-label="Photo"
        src={photo}
        aspectRatio={1}
        defaultCrop={start}
        controller={cropper}
        onCropCommit={setSaved}
      >
        <ImageCropperViewport>
          <ImageCropperImage alt="A rowing boat on a green mountain lake" />
          <ImageCropperSelection>
            <ImageCropperGrid />
            <ImageCropperHandles />
          </ImageCropperSelection>
        </ImageCropperViewport>
      </ImageCropper>
      <div className="flex items-center gap-3">
        <Button
          variant="outline"
          size="sm"
          onClick={() => cropper.setCrop(start)}
        >
          Reset
        </Button>
        <span className="text-sm text-muted-foreground tabular-nums">
          {saved
            ? `Saved ${Math.round(saved.width)} × ${Math.round(saved.height)} at ${Math.round(saved.x)}, ${Math.round(saved.y)}`
            : "Move the crop to save it."}
        </span>
      </div>
    </div>
  )
}

export { UiSdkImageCropperControlled }

Devre dışı bir kırpıcı odak almaz, değişmez; yakınlaştırması ve düğmeleri de devre dışıdır.

import {
  ImageCropper,
  ImageCropperHandles,
  ImageCropperImage,
  ImageCropperRotate,
  ImageCropperSelection,
  ImageCropperViewport,
  ImageCropperZoom,
} from "@/components/ui/ui-sdk-image-cropper"

const photo =
  "https://images.unsplash.com/photo-1501785888041-af3ef285b470?w=1200&q=80&auto=format&fit=crop"

function UiSdkImageCropperDisabled() {
  return (
    <ImageCropper
      aria-label="Photo"
      src={photo}
      aspectRatio={16 / 9}
      disabled
      className="max-w-md"
    >
      <ImageCropperViewport>
        <ImageCropperImage alt="A rowing boat on a green mountain lake" />
        <ImageCropperSelection>
          <ImageCropperHandles />
        </ImageCropperSelection>
      </ImageCropperViewport>
      <div className="flex items-center gap-2">
        <ImageCropperZoom />
        <ImageCropperRotate />
      </div>
    </ImageCropper>
  )
}

export { UiSdkImageCropperDisabled }
TuşAction
←→↑↓Seçimi taşır; bir tutamaçtaysa kenarlarını taşır.
Shift←On kat daha uzağa taşır.
+-Kırpmanın ortasına göre yakınlaştırır veya uzaklaştırır.
RGörseli saat yönünde çeyrek tur döndürür, Shift ile geri döndürür.
EscSürükleme sırasında kırpmayı sürüklemenin başladığı yere geri koyar.

Kırpıcı, aria-label ile adlandırılan bir gruptur. Seçim ve her tutamaç kırpmayı sözcüklerle söyleyen kaydırıcılardır (ör. “300 by 300 pixels, at 50, 0”), yakınlaştırma da düzeyini söyler (ör. “125%”). Bu adlar İngilizcedir; her parçaya sayfanızın dilinde aria-label verin.

Izgara yalnızca sürüklerken belirir, azaltılmış harekette hiç görünmez. Dokunmatik ekranlarda tutamaçların dokunma alanı büyür, sürükleme sırasında sayfa kaymaz; zorunlu renkler modunda seçim sistem renginde bir kenarlık alır.

Her parça aynı adlı UI SDK parçasını sarar ve render ile parçanın durumuna bağlı bir fonksiyon olabilen className dahil tüm prop’larını kabul eder.

PropTürVarsayılan
src
string–
cropGörsel pikselleri cinsinden kırpma; rotation, flipX ve flipY ile.
ImageCrop–
defaultCrop
ImageCrop–
onCropChange
(crop, details) => void–
onCropCommitBir sürükleme, tuş basışı veya tekerlek bittiğinde çalışır.
(crop, details) => void–
mode
"box" | "frame""box"
aspectRatio
number–
shape
"rectangle" | "circle""rectangle"
minWidth
number–
minHeight
number–
maxZoom
number10
disabled
booleanfalse
controllercreateImageCropperController() ile oluşturulur.
ImageCropperController–
ÖznitelikAçıklama
data-slot="image-cropper"CSS’te kırpıcıyı hedefler.
data-modebox veya frame.
data-shaperectangle veya circle.
data-loadingGörsel yüklenene kadar bulunur.
data-errorGörsel yüklenemediğinde bulunur.
data-draggingBir işaretçi sürüklerken bulunur.
PropTürVarsayılan
render
ReactElement | (props, state) => ReactElement–
ÖznitelikAçıklama
data-slot="image-cropper-viewport"CSS’te görüntü alanını hedefler.
--image-cropper-aspect-ratioGörsel yüklendikten sonra genişliğinin yüksekliğine oranı. Görüntü alanı bunu en-boy oranı olarak kullanır.
PropTürVarsayılan
altZorunlu.
string–
render
ReactElement | (props, state) => ReactElement–
PropTürVarsayılan
aria-label
string"Crop"
render
ReactElement | (props, state) => ReactElement–
ÖznitelikAçıklama
data-slot="image-cropper-selection"CSS’te seçimi hedefler.
PropTürVarsayılan
childrenVarsayılan olarak üçe bölen çizgiler.
ReactNode–
PropTürVarsayılan
positionYeniden boyutlandırdığı kenar veya köşe.
"n" | "e" | "s" | "w" | "ne" | "nw" | "se" | "sw"–

ImageCropperHandles sekizini de çizer. Frame modunda tutamaçlar görünmez.

1’den (en büyük kırpma) maxZoom değerine kadar giden yerel bir range input.

PropTürVarsayılan
childrenVarsayılan olarak Rotate etiketli bir simge. Alt öğe verilirse küçük bir metin düğmesidir.
ReactNode–
PropTürVarsayılan
direction
"horizontal" | "vertical""horizontal"
childrenVarsayılan olarak İngilizce etiketli bir simge.
ReactNode–