Slideshow with Camera
The Tldraw
component provides the tldraw editor as a regular React component. You can put this component anywhere in your React project. In this example, we make the component take up the height and width of the container.
By default, the component does not persist between refreshes or sync locally between tabs. To keep your work after a refresh, check the persistenceKey
example.
import { useEffect, useState } from 'react'
import {
Editor,
TLFrameShape,
Tldraw,
createShapeId,
stopEventPropagation,
transact,
useValue,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { SLIDE_MARGIN, SLIDE_SIZE, SlidesProvider, useSlides } from './SlidesManager'
export default function SlideShowExample() {
return (
<div className="tldraw__editor">
<SlidesProvider>
<InsideSlidesContext />
</SlidesProvider>
</div>
)
}
function InsideSlidesContext() {
const [editor, setEditor] = useState<Editor | null>(null)
const slides = useSlides()
const currentSlide = useValue('currentSlide', () => slides.getCurrentSlide(), [slides])
useEffect(() => {
if (!editor) return
const nextBounds = {
x: currentSlide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
y: 0,
w: SLIDE_SIZE.w,
h: SLIDE_SIZE.h,
}
editor.setCameraOptions({
constraints: {
bounds: nextBounds,
behavior: 'contain',
initialZoom: 'fit-max',
baseZoom: 'fit-max',
origin: { x: 0.5, y: 0.5 },
padding: { x: 50, y: 50 },
},
})
editor.zoomToBounds(nextBounds, { force: true, animation: { duration: 500 } })
}, [editor, currentSlide])
const currentSlides = useValue('slides', () => slides.getCurrentSlides(), [slides])
useEffect(() => {
if (!editor) return
const ids = currentSlides.map((slide) => createShapeId(slide.id))
transact(() => {
for (let i = 0; i < currentSlides.length; i++) {
const shapeId = ids[i]
const slide = currentSlides[i]
const shape = editor.getShape(shapeId)
if (shape) {
if (shape.x === slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN)) continue
// if name is still Slide and number, e.g Slide 1, update it. Use regex to test
const regex = /Slide \d+/
let name = (shape as TLFrameShape).props.name
if (regex.test((shape as TLFrameShape).props.name)) {
name = `Slide ${slide.index + 1}`
}
editor.updateShape<TLFrameShape>({
id: shapeId,
type: 'frame',
x: slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
props: {
name,
},
})
} else {
editor.createShape<TLFrameShape>({
id: shapeId,
parentId: editor.getCurrentPageId(),
type: 'frame',
x: slide.index * (SLIDE_SIZE.w + SLIDE_MARGIN),
y: 0,
props: {
name: `Slide ${slide.index + 1}`,
w: SLIDE_SIZE.w,
h: SLIDE_SIZE.h,
},
})
}
}
})
const unsubs = [] as (() => void)[]
unsubs.push(
editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next) => {
if (
ids.includes(next.id) &&
(next as TLFrameShape).props.name === (prev as TLFrameShape).props.name
)
return prev
return next
})
)
unsubs.push(
editor.sideEffects.registerBeforeChangeHandler('instance_page_state', (prev, next) => {
next.selectedShapeIds = next.selectedShapeIds.filter((id) => !ids.includes(id))
if (next.hoveredShapeId && ids.includes(next.hoveredShapeId)) next.hoveredShapeId = null
return next
})
)
return () => {
unsubs.forEach((fn) => fn())
}
}, [currentSlides, editor])
const handleMount = (editor: Editor) => {
setEditor(editor)
}
return <Tldraw onMount={handleMount} components={components} />
}
function Slides() {
const slides = useSlides()
const currentSlides = useValue('slides', () => slides.getCurrentSlides(), [slides])
const lowestIndex = currentSlides[0].index
const highestIndex = currentSlides[currentSlides.length - 1].index
return (
<>
{/* {currentSlides.map((slide) => (
<div
key={slide.id}
style={{
position: 'absolute',
top: 0,
left: (SLIDE_SIZE.w + SLIDE_MARGIN) * slide.index,
width: SLIDE_SIZE.w,
height: SLIDE_SIZE.h,
backgroundColor: 'white',
border: '1px solid black',
pointerEvents: 'all',
}}
onPointerDown={(e) => {
if (slide.id !== slides.getCurrentSlideId()) {
stopEventPropagation(e)
slides.setCurrentSlide(slide.id)
}
}}
/>
))} */}
{currentSlides.slice(0, -1).map((slide) => (
<button
key={slide.id + 'between'}
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: (slide.index + 1) * (SLIDE_SIZE.w + SLIDE_MARGIN) - (SLIDE_MARGIN + 40) / 2,
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const newSlide = slides.newSlide(slide.index + 1)
slides.setCurrentSlide(newSlide.id)
}}
>
|
</button>
))}
<button
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: lowestIndex * (SLIDE_SIZE.w + SLIDE_MARGIN) - (40 + SLIDE_MARGIN * 0.1),
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const slide = slides.newSlide(lowestIndex - 1)
slides.setCurrentSlide(slide.id)
}}
>
{`+`}
</button>
<button
style={{
position: 'absolute',
top: SLIDE_SIZE.h / 2,
left: highestIndex * (SLIDE_SIZE.w + SLIDE_MARGIN) + (SLIDE_SIZE.w + SLIDE_MARGIN * 0.1),
width: 40,
height: 40,
pointerEvents: 'all',
}}
onPointerDown={stopEventPropagation}
onClick={() => {
const slide = slides.newSlide(highestIndex + 1)
slides.setCurrentSlide(slide.id)
}}
>
{`+`}
</button>
</>
)
}
function SlideControls() {
const slides = useSlides()
return (
<>
<button
style={{
pointerEvents: 'all',
position: 'absolute',
top: '50%',
left: 0,
width: 50,
height: 50,
}}
onPointerDown={stopEventPropagation}
onClick={() => slides.prevSlide()}
>
{`<`}
</button>
<button
style={{
pointerEvents: 'all',
position: 'absolute',
top: '50%',
right: 0,
width: 50,
height: 50,
}}
onPointerDown={stopEventPropagation}
onClick={() => slides.nextSlide()}
>
{`>`}
</button>
</>
)
}
const components = {
OnTheCanvas: Slides,
InFrontOfTheCanvas: SlideControls,
}
Prev
Fog of warNext
Image annotator