@feugene/granularity-media
v1.1.0
Published
Media components for @feugene/granularity: image cropping, camera capture, code scanning and video playback.
Maintainers
Readme
@feugene/granularity-media
Работа с изображением, камерой и видео для @feugene/granularity. Четыре
компонента, и они стоят рядом по сценарию: GrCameraCapture снимает кадр
камерой, GrImageCrop выбирает кадр из уже существующей картинки,
GrCodeScanner читает камерой QR и штрихкоды, GrVideoPlayer проигрывает
готовое видео своими элементами управления.
yarn add @feugene/granularity-mediaСвоих зависимостей у пакета нет: кадрирование держится на Canvas и браузерных
API, а не на библиотеке. Наружу объявлены только peer — ядро, granum и Vue.
Быстрый старт
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { GrImageCrop } from '@feugene/granularity-media/components/GrImageCrop'
const file = ref<File | null>(null)
const cropper = useTemplateRef('cropper')
async function save() {
const blob = await cropper.value?.crop()
if (blob)
await fetch('/api/avatar', { method: 'POST', body: blob })
}
</script>
<template>
<GrImageCrop ref="cropper" :src="file" shape="circle" :output="{ width: 256, height: 256 }" />
<button @click="save">
Сохранить
</button>
</template>Рамка неподвижна, двигается картинка
Обратная модель — рамка ездит по изображению — требует двух жестов вместо
одного, а её угловые ручки на телефоне меньше пальца. Здесь жест один: тянуть
картинку, а увеличение живёт отдельным контролом (по умолчанию GrSlider ядра,
заменяется слотом #controls).
Соотношение сторон кадра задаёт приложение (aspectRatio), а не пользователь:
место, куда картинка потом встанет, известно заранее.
Кадр считается в пикселях исходника
Без output результат получает размер захваченной области исходного файла, а
не окна на экране: окно почти всегда меньше картинки, и вывод по нему молча
ополовинил бы разрешение. Нужен точный размер — он задаётся явно.
Что внутри
| | |
| --- | --- |
| Жест | перетаскивание на useDragGesture ядра: оборванный жест откатывается, а не коммитит |
| Клавиатура | стрелки двигают кадр, +/- меняют увеличение, Home сбрасывает |
| Экспорт | crop() отдаёт Blob; тип и качество — output.type, output.quality |
| Формы кадра | прямоугольник и круг (shape="circle" — маска показа, результат остаётся прямоугольным) |
| Серверный рендер | браузерного API в setup нет, оборачивать в <ClientOnly> не нужно |
Вес гранулярного импорта
| Что берут | gzip | от бареля |
| --- | ---: | ---: |
| весь пакет из корня | 14.5 kB | 100 % |
| самый лёгкий компонент — GrVideoPlayer | 3.6 kB | 25 % |
| медианный компонент — GrCodeScanner | 4.1 kB | 28 % |
| 4 самых тяжёлых вместе | 14.7 kB | 102 % |
Числа не складываются: общий код посчитан в каждой строке заново, а платится один раз —
поэтому набор компонентов и показан объединением, а не суммой. Это верхняя граница: gzip всего,
что подпуть тянет из dist, а бандлер приложения трясёт дальше и минифицирует повторно.
Вес каждого компонента — docs/entry-sizes.md.
Подключение к сборке
Пакет — рядом с ядром, именем: granum читает манифест из его dist, а не
сканирует node_modules. Авто-импорт — GranularityResolver() из
@feugene/unplugin-granularity >=1.1.0: пакет он найдёт в package.json приложения сам.
// granum.config.ts
import { defineGranumConfig } from '@feugene/granum/vite'
import { windEngine } from '@feugene/granum-engine-wind'
export default defineGranumConfig({
engine: windEngine(),
providers: ['@feugene/granularity', '@feugene/granularity-media'],
components: ['@feugene/granularity-media:GrImageCrop'],
})Классы компонентов написаны против словаря unocss/preset-wind3+granum@66.
Движок другого диалекта заставит granum сообщить provider-dialect-mismatch и
перечислить отброшенные классы, а не отрисовать компоненты наполовину.
Локали подключаются тем же createFintI18n, что и словари ядра — блок
GR_MEDIA_I18N_BLOCK.
Снимок камерой
<GrCameraCapture :aspect-ratio="1" @capture="blob => upload(blob)" />Камера включается только по кнопке: запрос разрешения, всплывший сам по себе, отклоняют не глядя — а второй раз браузер уже не спросит, потому что решение запоминается для сайта целиком.
Отказ разведён на четыре состояния (denied, missing, busy, insecure),
потому что действия пользователя в них разные: «разрешите доступ» при
отсутствующей камере отправляет искать настройку, которой нет, а на http://
объекта navigator.mediaDevices не существует вовсе.
Превью фронтальной камеры зеркальное, снимок — нет: иначе текст на визитке или в документе уехал бы в зазеркалье.
Чтение кодов
<GrCodeScanner :formats="['qr_code']" @detect="codes => open(codes[0].value)" />Декодера пакет не несёт. Нативный BarcodeDetector есть в Chrome и Edge, а
Safari и Firefox закрываются детектором, который передаёт приложение
(:detector): встроить библиотеку значило бы навязать самую тяжёлую
зависимость и тем, кто взял один кроп. Готовый рецепт на @zxing/browser — на
странице компонента.
Один код в кадре даёт одно событие: камера отдаёт десятки кадров в секунду, и
без фильтра приложение оформило бы двадцать заказов вместо одного. Режим
приёмки, где сканируют одинаковые упаковки подряд, включается continuous.
Видео со своими элементами управления
<GrVideoPlayer :src="clip" poster="/preview.jpg" muted />Нативные controls в каждом браузере выглядят по-своему и не знают ни про темы,
ни про размеры дизайн-системы. Полный экран запрашивается у рамки, а не у
<video>: иначе браузер подменил бы эти элементы управления своими — вместе с
клавиатурой и подписями.
Документация
docs/components.md— состав пакета;docs/components/GrImageCrop.md,docs/components/GrCameraCapture.mdиdocs/components/GrCodeScanner.mdиdocs/components/GrVideoPlayer.md— страницы компонентов;docs/keyboard.md,docs/ssr.md— сквозные правила пакета.
