tldraw
Version:
A tiny little drawing editor.
159 lines (158 loc) • 5.59 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import {
BaseBoxShapeUtil,
HTMLContainer,
MediaHelpers,
WeakCache,
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";
const videoSvgExportCache = new WeakCache();
class VideoShapeUtil extends BaseBoxShapeUtil {
static type = "video";
static props = videoShapeProps;
static migrations = videoShapeMigrations;
options = {
autoplay: true
};
canEdit() {
return true;
}
isAspectRatioLocked() {
return true;
}
getDefaultProps() {
return {
w: 100,
h: 100,
assetId: null,
autoplay: this.options.autoplay,
url: "",
altText: "",
// Not used, but once upon a time were used to sync video state between users
time: 0,
playing: true
};
}
getAriaDescriptor(shape) {
return shape.props.altText;
}
component(shape) {
return /* @__PURE__ */ jsx(VideoShape, { shape });
}
indicator(shape) {
return /* @__PURE__ */ jsx("rect", { width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h) });
}
useLegacyIndicator() {
return false;
}
getIndicatorPath(shape) {
const path = new Path2D();
path.rect(0, 0, shape.props.w, shape.props.h);
return path;
}
async toSvg(shape, ctx) {
const props = shape.props;
if (!props.assetId) return null;
const asset = this.editor.getAsset(props.assetId);
if (!asset) return null;
const src = await videoSvgExportCache.get(asset, async () => {
const assetUrl = await ctx.resolveAssetUrl(asset.id, props.w);
if (!assetUrl) return null;
const video = await MediaHelpers.loadVideo(assetUrl);
return await MediaHelpers.getVideoFrameAsDataUrl(video, 0);
});
if (!src) return null;
return /* @__PURE__ */ jsx("image", { href: src, width: props.w, height: props.h, "aria-label": shape.props.altText });
}
}
const VideoShape = memo(function VideoShape2({ shape }) {
const editor = useEditor();
const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getEfficientZoomLevel() >= 110;
const isEditing = useIsEditing(shape.id);
const prefersReducedMotion = usePrefersReducedMotion();
const { Spinner } = useEditorComponents();
const { asset, url } = useImageOrVideoAsset({
shapeId: shape.id,
assetId: shape.props.assetId,
width: shape.props.w
});
const rVideo = useRef(null);
const [isLoaded, setIsLoaded] = useState(false);
const handleLoadedData = useCallback((e) => {
const video = e.currentTarget;
if (!video) return;
setIsLoaded(true);
}, []);
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
const fullscreenChange = () => setIsFullscreen(document.fullscreenElement === rVideo.current);
document.addEventListener("fullscreenchange", fullscreenChange);
return () => document.removeEventListener("fullscreenchange", fullscreenChange);
});
useEffect(() => {
const video = rVideo.current;
if (!video) return;
if (isEditing) {
if (document.activeElement !== video) {
video.focus();
}
}
}, [isEditing, isLoaded]);
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
HTMLContainer,
{
id: shape.id,
style: {
color: "var(--tl-color-text-3)",
backgroundColor: asset ? "transparent" : "var(--tl-color-low)",
border: asset ? "none" : "1px solid var(--tl-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: shape.props.autoplay && !prefersReducedMotion,
muted: true,
loop: true,
disableRemotePlayback: true,
disablePictureInPicture: true,
controls: isEditing && showControls,
onLoadedData: handleLoadedData,
hidden: !isLoaded,
"aria-label": shape.props.altText,
children: /* @__PURE__ */ jsx("source", { src: url })
},
url
),
!isLoaded && Spinner && /* @__PURE__ */ jsx(Spinner, {})
] }) : null }) })
}
),
"url" in shape.props && shape.props.url && /* @__PURE__ */ jsx(HyperlinkButton, { url: shape.props.url })
] });
});
export {
VideoShapeUtil
};
//# sourceMappingURL=VideoShapeUtil.mjs.map