kopia lustrzana https://github.com/Tldraw/Tldraw
224 wiersze
5.6 KiB
TypeScript
224 wiersze
5.6 KiB
TypeScript
/* eslint-disable react-hooks/rules-of-hooks */
|
|
import {
|
|
BaseBoxShapeUtil,
|
|
HTMLContainer,
|
|
TLVideoShape,
|
|
toDomPrecision,
|
|
useIsEditing,
|
|
videoShapeMigrations,
|
|
videoShapeProps,
|
|
} from '@tldraw/editor'
|
|
import { ReactEventHandler, useCallback, useEffect, useRef, useState } from 'react'
|
|
import { BrokenAssetIcon } from '../shared/BrokenAssetIcon'
|
|
import { HyperlinkButton } from '../shared/HyperlinkButton'
|
|
import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
|
|
|
|
/** @public */
|
|
export class VideoShapeUtil extends BaseBoxShapeUtil<TLVideoShape> {
|
|
static override type = 'video' as const
|
|
static override props = videoShapeProps
|
|
static override migrations = videoShapeMigrations
|
|
|
|
override canEdit = () => true
|
|
override isAspectRatioLocked = () => true
|
|
|
|
override getDefaultProps(): TLVideoShape['props'] {
|
|
return {
|
|
w: 100,
|
|
h: 100,
|
|
assetId: null,
|
|
time: 0,
|
|
playing: true,
|
|
url: '',
|
|
}
|
|
}
|
|
|
|
component(shape: TLVideoShape) {
|
|
const { editor } = this
|
|
const showControls = editor.getShapeGeometry(shape).bounds.w * editor.getZoomLevel() >= 110
|
|
const asset = shape.props.assetId ? editor.getAsset(shape.props.assetId) : null
|
|
const { time, playing } = shape.props
|
|
const isEditing = useIsEditing(shape.id)
|
|
const prefersReducedMotion = usePrefersReducedMotion()
|
|
|
|
const rVideo = useRef<HTMLVideoElement>(null!)
|
|
|
|
const handlePlay = useCallback<ReactEventHandler<HTMLVideoElement>>(
|
|
(e) => {
|
|
const video = e.currentTarget
|
|
if (!video) return
|
|
|
|
editor.updateShapes([
|
|
{
|
|
type: 'video',
|
|
id: shape.id,
|
|
props: {
|
|
playing: true,
|
|
time: video.currentTime,
|
|
},
|
|
},
|
|
])
|
|
},
|
|
[shape.id, editor]
|
|
)
|
|
|
|
const handlePause = useCallback<ReactEventHandler<HTMLVideoElement>>(
|
|
(e) => {
|
|
const video = e.currentTarget
|
|
if (!video) return
|
|
|
|
editor.updateShapes([
|
|
{
|
|
type: 'video',
|
|
id: shape.id,
|
|
props: {
|
|
playing: false,
|
|
time: video.currentTime,
|
|
},
|
|
},
|
|
])
|
|
},
|
|
[shape.id, editor]
|
|
)
|
|
|
|
const handleSetCurrentTime = useCallback<ReactEventHandler<HTMLVideoElement>>(
|
|
(e) => {
|
|
const video = e.currentTarget
|
|
if (!video) return
|
|
|
|
if (isEditing) {
|
|
editor.updateShapes([
|
|
{
|
|
type: 'video',
|
|
id: shape.id,
|
|
props: {
|
|
time: video.currentTime,
|
|
},
|
|
},
|
|
])
|
|
}
|
|
},
|
|
[isEditing, shape.id, editor]
|
|
)
|
|
|
|
const [isLoaded, setIsLoaded] = useState(false)
|
|
|
|
const handleLoadedData = useCallback<ReactEventHandler<HTMLVideoElement>>(
|
|
(e) => {
|
|
const video = e.currentTarget
|
|
if (!video) return
|
|
if (time !== video.currentTime) {
|
|
video.currentTime = time
|
|
}
|
|
|
|
if (!playing) {
|
|
video.pause()
|
|
}
|
|
|
|
setIsLoaded(true)
|
|
},
|
|
[playing, time]
|
|
)
|
|
|
|
// If the current time changes and we're not editing the video, update the video time
|
|
useEffect(() => {
|
|
const video = rVideo.current
|
|
if (!video) return
|
|
|
|
if (isLoaded && !isEditing && time !== video.currentTime) {
|
|
video.currentTime = time
|
|
}
|
|
|
|
if (isEditing) {
|
|
if (document.activeElement !== video) {
|
|
video.focus()
|
|
}
|
|
}
|
|
}, [isEditing, isLoaded, time])
|
|
|
|
useEffect(() => {
|
|
if (prefersReducedMotion) {
|
|
const video = rVideo.current
|
|
if (!video) return
|
|
video.pause()
|
|
video.currentTime = 0
|
|
}
|
|
}, [rVideo, prefersReducedMotion])
|
|
|
|
return (
|
|
<>
|
|
<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)',
|
|
}}
|
|
>
|
|
<div className="tl-counter-scaled">
|
|
<div className="tl-video-container">
|
|
{asset?.props.src ? (
|
|
<video
|
|
ref={rVideo}
|
|
style={isEditing ? { pointerEvents: 'all' } : undefined}
|
|
className={`tl-video tl-video-shape-${shape.id.split(':')[1]}`}
|
|
width="100%"
|
|
height="100%"
|
|
draggable={false}
|
|
playsInline
|
|
autoPlay
|
|
muted
|
|
loop
|
|
disableRemotePlayback
|
|
disablePictureInPicture
|
|
controls={isEditing && showControls}
|
|
onPlay={handlePlay}
|
|
onPause={handlePause}
|
|
onTimeUpdate={handleSetCurrentTime}
|
|
onLoadedData={handleLoadedData}
|
|
hidden={!isLoaded}
|
|
>
|
|
<source src={asset.props.src} />
|
|
</video>
|
|
) : (
|
|
<BrokenAssetIcon />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</HTMLContainer>
|
|
{'url' in shape.props && shape.props.url && (
|
|
<HyperlinkButton url={shape.props.url} zoomLevel={editor.getZoomLevel()} />
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
indicator(shape: TLVideoShape) {
|
|
return <rect width={toDomPrecision(shape.props.w)} height={toDomPrecision(shape.props.h)} />
|
|
}
|
|
|
|
override toSvg(shape: TLVideoShape) {
|
|
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
|
|
const image = document.createElementNS('http://www.w3.org/2000/svg', 'image')
|
|
image.setAttributeNS('http://www.w3.org/1999/xlink', 'href', serializeVideo(shape.id))
|
|
image.setAttribute('width', shape.props.w.toString())
|
|
image.setAttribute('height', shape.props.h.toString())
|
|
g.appendChild(image)
|
|
|
|
return g
|
|
}
|
|
}
|
|
|
|
// Function from v1, could be improved but explicitly using this.model.time (?)
|
|
function serializeVideo(id: string): string {
|
|
const splitId = id.split(':')[1]
|
|
const video = document.querySelector(`.tl-video-shape-${splitId}`) as HTMLVideoElement
|
|
if (video) {
|
|
const canvas = document.createElement('canvas')
|
|
canvas.width = video.videoWidth
|
|
canvas.height = video.videoHeight
|
|
canvas.getContext('2d')!.drawImage(video, 0, 0)
|
|
return canvas.toDataURL('image/png')
|
|
} else throw new Error('Video with not found when attempting serialization.')
|
|
}
|