Vitral 0.2
Mídia

Cropper

Um retângulo de recorte sobre uma imagem, sem dependências: trave uma proporção, use uma máscara circular, gire e espelhe, e recorte no servidor ou no navegador.

Importação

main.ts
import { Cropper } from '@vitral/vue';

Avatar

Um círculo trava a proporção em um e dispensa as alças: arraste o recorte, ou dê zoom nele. A pré-visualização ao lado é a mesma imagem sob uma caixa menor, não uma segunda renderização.

A portrait

Use as setas para mover o recorte, Shift com uma seta para redimensioná-lo, Home e End para os cantos.

<script setup lang="ts">import { Button, Cropper, type CropValue, StackPanel } from '@vitral/vue';import { ref, useTemplateRef } from 'vue'; const portrait = 'https://picsum.photos/id/1027/900/1200';const avatar = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false }); // The file, when the file is what you want: a canvas, then a Blob to upload.const cropper = useTemplateRef<InstanceType<typeof Cropper>>('avatarCropper');const saved = ref<string | null>(null);const savedSize = ref(0); async function save() {    const blob = await cropper.value?.toBlob('image/png', { width: 256 });    if (!blob) return;    if (saved.value) URL.revokeObjectURL(saved.value);    saved.value = URL.createObjectURL(blob);    savedSize.value = blob.size;}</script> <template>    <StackPanel spacing="1rem" style="flex: 1 1 100%; min-width: 0">        <Cropper ref="avatarCropper" v-model="avatar" :src="portrait" shape="circle" alt="A portrait" height="18rem" :preview-size="96" />        <StackPanel orientation="horizontal" spacing="0.625rem" align="center" wrap>            <Button label="Crop to a 256px PNG" size="small" @click="save" />            <small v-if="saved" style="color: var(--vt-text-muted-color)">{{ Math.round(savedSize / 1024) }} kB</small>            <img v-if="saved" :src="saved" alt="The cropped avatar" width="48" height="48" style="border-radius: 50%" />        </StackPanel>    </StackPanel></template>

Uma proporção, com alças

Oito alças para o ponteiro, ocultas para tecnologias assistivas: o próprio retângulo é a única parada de tabulação.

A landscape

Use as setas para mover o recorte, Shift com uma seta para redimensioná-lo, Home e End para os cantos.

{ x: 160, y: 90, width: 1280, height: 720, rotate: 0 }
<script setup lang="ts">import { Cropper, type CropValue } from '@vitral/vue';import { ref } from 'vue'; const photo = 'https://picsum.photos/id/1015/1600/900';const banner = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false }); const rounded = (value: CropValue) => `{ x: ${Math.round(value.x)}, y: ${Math.round(value.y)}, width: ${Math.round(value.width)}, height: ${Math.round(value.height)}, rotate: ${value.rotate} }`;</script> <template>    <Cropper v-model="banner" :src="photo" aspect="16:9" alt="A landscape" height="18rem" />    <small style="color: var(--vt-text-muted-color)">{{ rounded(banner) }}</small></template>

Sem os terços

A guia fica ligada num retângulo e desligada num círculo, onde a regra dos terços não significa nada. `grid` decide por cima, num sentido ou no outro.

A landscape

Use as setas para mover o recorte, Shift com uma seta para redimensioná-lo, Home e End para os cantos.

<script setup lang="ts">import { Cropper, type CropValue } from '@vitral/vue';import { ref } from 'vue'; const photo = 'https://picsum.photos/id/1015/1600/900';const plain = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false });</script> <template>    <Cropper v-model="plain" :src="photo" aspect="4:3" :grid="false" alt="A landscape" height="16rem" /></template>

Livre, com proporções e giros

Um seletor trava a proporção; os giros de um quarto e os espelhamentos acompanham o recorte, que continua apontando para a mesma parte da imagem.

A landscape

Use as setas para mover o recorte, Shift com uma seta para redimensioná-lo, Home e End para os cantos.

const crop = ref<CropValue>({ x: 160, y: 90, width: 1280, height: 720, rotate: 0 });
<script setup lang="ts">import { Cropper, type CropValue } from '@vitral/vue';import { computed, ref } from 'vue'; const photo = 'https://picsum.photos/id/1015/1600/900';const free = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false }); const aspects = [    { label: 'Free', value: 'free' },    { label: '1:1', value: '1:1' },    { label: '4:3', value: '4:3' },    { label: '16:9', value: '16:9' }]; const model = computed(() => {    const { x, y, width, height, rotate } = free.value;    return `const crop = ref<CropValue>({ x: ${Math.round(x)}, y: ${Math.round(y)}, width: ${Math.round(width)}, height: ${Math.round(height)}, rotate: ${rotate} });`;});</script> <template>    <Cropper v-model="free" :src="photo" :aspects="aspects" rotatable alt="A landscape" height="20rem" />    <code style="font-size: 0.75rem; color: var(--vt-text-muted-color)">{{ model }}</code></template>

Num diálogo

Onde um cropper costuma morar: uma foto é escolhida, recortada sobre a página, e o perfil só muda depois que ela é aceita. O diálogo dá a largura ao palco, então o recorte é medido em relação ao tamanho em que é de fato exibido.

AF
<script setup lang="ts">import { Avatar, Button, Cropper, Dialog, StackPanel, type CropValue } from '@vitral/vue';import { ref, useTemplateRef } from 'vue'; // The whole flow, which is where a cropper usually lives: a picture is// chosen, cropped in a dialog, and only then does the profile change.const portrait = 'https://picsum.photos/id/1027/900/1200';const dialog = ref(false);const pending = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false });const profile = ref<string | null>(null);const modalCropper = useTemplateRef<InstanceType<typeof Cropper>>('modalCropper'); async function apply() {    const blob = await modalCropper.value?.toBlob('image/png', { width: 192 });    dialog.value = false;    if (!blob) return;    if (profile.value) URL.revokeObjectURL(profile.value);    profile.value = URL.createObjectURL(blob);}</script> <template>    <StackPanel orientation="horizontal" spacing="0.625rem" align="center" wrap>        <Avatar :image="profile ?? undefined" :label="profile ? undefined : 'AF'" shape="circle" size="large" :alt="profile ? 'Your photo' : undefined" />        <Button :label="profile ? 'Change the photo' : 'Upload a photo'" severity="secondary" variant="outlined" size="small" @click="dialog = true" />        <small v-if="profile" style="color: var(--vt-text-muted-color)">Cropped to a 192px PNG</small>    </StackPanel>     <Dialog v-model:visible="dialog" modal header="Crop your photo" :style="{ width: 'min(30rem, 92vw)' }">        <Cropper ref="modalCropper" v-model="pending" :src="portrait" shape="circle" alt="The photograph being cropped" height="16rem" />        <template #footer>            <Button label="Cancel" severity="secondary" variant="text" @click="dialog = false" />            <Button label="Use this photo" @click="apply" />        </template>    </Dialog></template>

Como usar

A portrait

Use as setas para mover o recorte, Shift com uma seta para redimensioná-lo, Home e End para os cantos.

<script setup lang="ts">import { Button, Cropper, type CropValue } from '@vitral/vue';import { ref, useTemplateRef } from 'vue'; const src = 'https://picsum.photos/id/1027/900/1200';const crop = ref<CropValue>({ x: 0, y: 0, width: 0, height: 0, rotate: 0, flipX: false, flipY: false });const cropper = useTemplateRef<InstanceType<typeof Cropper>>('cropper'); // The file, when you want the file rather than the numbers.async function upload() {    const blob = await cropper.value?.toBlob('image/png', { width: 256 });    if (blob) console.log(blob);}</script> <template>    <Cropper ref="cropper" v-model="crop" :src="src" shape="circle" :preview-size="96" alt="A portrait" />    <Button label="Get the file" size="small" @click="upload" /></template>

API

Lido de packages/vue/src/components/Cropper/types.ts, então diz o que o componente aceita de fato.

Props

NomeTipoDescrição
srcstringA imagem. Qualquer coisa que um `<img>` aceite, incluindo uma URL de dados ou de objeto.
altstringDescreve a imagem, não o controle. Sem ele a imagem é decorativa.
aspectnumber | string | nullTrava o recorte numa proporção: `1`, `'16:9'`, ou null para deixá-lo livre.
aspectsreadonly CropAspect[]Proporções a oferecer na barra de ferramentas. Omita-o e nenhum seletor é desenhado.
shape'rect' | 'circle'`'circle'` mascara o recorte e a prévia, e trava a proporção em 1.
handlesbooleanDesenha as oito alças de redimensionamento. O padrão é true para um retângulo e false para um círculo.
rotatablebooleanOferece os giros de um quarto de volta e os espelhamentos.
zoomablebooleanOferece um controle deslizante de zoom.
gridbooleanDesenha os terços sobre o recorte enquanto ele é movido. Ligado por padrão num retângulo e desligado num círculo, onde a regra dos terços não significa nada: a guia serve para compor um quadro, e um recorte redondo não tem cantos para onde compor.
minWidthnumberO menor recorte, nos pixels da própria imagem.
minHeightnumber—
heightnumber | stringA altura do palco: um número é em pixels.
previewSizenumberO tamanho da prévia ao vivo, em pixels. Zero não desenha nenhuma.
disabledboolean—

Mais pt, dt e unstyled de BaseProps; veja pass-through e modo sem estilo.

Emits

EventoPayloadDescrição
update:modelValuevalue: CropValueO recorte, à medida que muda.
loadevent: { width: number; height: number }A imagem carregou e seu tamanho natural é conhecido.
changevalue: CropValueUm arrasto do recorte ou de uma alça terminou.

Slots

NomeProps do slotDescrição
toolbar(props: { value: CropValue; reset: () => void })Substitui a barra de ferramentas.
preview(props: { value: CropValue })Ao lado da prévia, ou no lugar dela.