UNPKG

tldraw

Version:

A tiny little drawing editor.

144 lines (143 loc) 5.05 kB
import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { BaseBoxShapeUtil, HTMLContainer, MediaHelpers, toDomPrecision, useEditor, useEditorComponents, useIsEditing, videoShapeMigrations, videoShapeProps } from "@tldraw/editor"; import classNames from "classnames"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { BrokenAssetIcon } from "../shared/BrokenAssetIcon.mjs"; import { HyperlinkButton } from "../shared/HyperlinkButton.mjs"; import { useImageOrVideoAsset } from "../shared/useImageOrVideoAsset.mjs"; import { usePrefersReducedMotion } from "../shared/usePrefersReducedMotion.mjs"; class VideoShapeUtil extends BaseBoxShapeUtil { static type = "video"; static props = videoShapeProps; static migrations = videoShapeMigrations; canEdit() { return true; } isAspectRatioLocked() { return true; } getDefaultProps() { return { w: 100, h: 100, assetId: null, time: 0, playing: true, url: "" }; } component(shape) { return /* @__PURE__ */ jsx(VideoShape, { shape }); } indicator(shape) { return /* @__PURE__ */ jsx("rect", { width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h) }); } async toSvg(shape) { const image = await serializeVideo(this.editor, shape); if (!image) return null; return /* @__PURE__ */ jsx("image", { href: image, width: shape.props.w, height: shape.props.h }); } } const VideoShape = memo(function VideoShape2({ shape }) { const editor = useEditor(); const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getZoomLevel() >= 110; const isEditing = useIsEditing(shape.id); const prefersReducedMotion = usePrefersReducedMotion(); const { Spinner } = useEditorComponents(); const { asset, url } = useImageOrVideoAsset({ shapeId: shape.id, assetId: shape.props.assetId }); const rVideo = useRef(null); const [isLoaded, setIsLoaded] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const fullscreenChange = () => setIsFullscreen(document.fullscreenElement === rVideo.current); document.addEventListener("fullscreenchange", fullscreenChange); return () => document.removeEventListener("fullscreenchange", fullscreenChange); }); const handleLoadedData = useCallback((e) => { const video = e.currentTarget; if (!video) return; setIsLoaded(true); }, []); useEffect(() => { const video = rVideo.current; if (!video) return; if (isEditing) { if (document.activeElement !== video) { video.focus(); } } }, [isEditing, isLoaded]); useEffect(() => { if (prefersReducedMotion) { const video = rVideo.current; if (!video) return; video.pause(); video.currentTime = 0; } }, [rVideo, prefersReducedMotion]); return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( HTMLContainer, { id: shape.id, style: { color: "var(--color-text-3)", backgroundColor: asset ? "transparent" : "var(--color-low)", border: asset ? "none" : "1px solid var(--color-low-border)" }, children: /* @__PURE__ */ jsx("div", { className: "tl-counter-scaled", children: /* @__PURE__ */ jsx("div", { className: "tl-video-container", children: !asset ? /* @__PURE__ */ jsx(BrokenAssetIcon, {}) : Spinner && !asset.props.src ? /* @__PURE__ */ jsx(Spinner, {}) : url ? /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsx( "video", { ref: rVideo, style: isEditing ? { pointerEvents: "all" } : !isLoaded ? { display: "none" } : void 0, className: classNames("tl-video", `tl-video-shape-${shape.id.split(":")[1]}`, { "tl-video-is-fullscreen": isFullscreen }), width: "100%", height: "100%", draggable: false, playsInline: true, autoPlay: true, muted: true, loop: true, disableRemotePlayback: true, disablePictureInPicture: true, controls: isEditing && showControls, onLoadedData: handleLoadedData, hidden: !isLoaded, children: /* @__PURE__ */ jsx("source", { src: url }) } ), !isLoaded && Spinner && /* @__PURE__ */ jsx(Spinner, {}) ] }) : null }) }) } ), "url" in shape.props && shape.props.url && /* @__PURE__ */ jsx(HyperlinkButton, { url: shape.props.url }) ] }); }); async function serializeVideo(editor, shape) { const assetUrl = await editor.resolveAssetUrl(shape.props.assetId, { shouldResolveToOriginal: true }); if (!assetUrl) return null; const video = await MediaHelpers.loadVideo(assetUrl); return MediaHelpers.getVideoFrameAsDataUrl(video, 0); } export { VideoShapeUtil }; //# sourceMappingURL=VideoShapeUtil.mjs.map