tldraw
Version:
A tiny little drawing editor.
144 lines (143 loc) • 5.05 kB
JavaScript
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