Skip to content

Commit

Permalink
Split rendering strategy into context
Browse files Browse the repository at this point in the history
  • Loading branch information
stephenwf committed Jan 4, 2025
1 parent 0ef44e9 commit 2c6354d
Show file tree
Hide file tree
Showing 20 changed files with 603 additions and 245 deletions.
3 changes: 1 addition & 2 deletions src/canvas-panel/Viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useMemo, useState } from 'react';
import { AtlasAuto, Preset, AtlasProps, ModeContext } from '@atlas-viewer/atlas';
import { ErrorBoundary } from 'react-error-boundary';
import { ContextBridge, useContextBridge, useCustomContextBridge } from '../context/ContextBridge';
Expand All @@ -7,7 +7,6 @@ import { DefaultCanvasFallback } from './render/DefaultCanvasFallback';
import { ViewerPresetContext } from '../context/ViewerPresetContext';
import { SetOverlaysReactContext, SetPortalReactContext } from './context/overlays';
import { WorldSizeContext } from './context/world-size';
import { useCanvas } from '../hooks/useCanvas';

export function Viewer({
children,
Expand Down
291 changes: 50 additions & 241 deletions src/canvas-panel/render/Canvas.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
import { createStylesHelper } from '@iiif/helpers/styles';
import { RenderImage } from './Image';
import React, { Fragment, ReactNode, useEffect, useLayoutEffect, useMemo } from 'react';
import { BoxStyle, HTMLPortal } from '@atlas-viewer/atlas';
import { useVirtualAnnotationPageContext } from '../../hooks/useVirtualAnnotationPageContext';
import { StrategyActions, useRenderingStrategy } from '../../hooks/useRenderingStrategy';
import { useVault } from '../../hooks/useVault';
import { useResourceEvents } from '../../hooks/useResourceEvents';
import { useThumbnail } from '../../hooks/useThumbnail';
import { useCanvas } from '../../hooks/useCanvas';
import { RenderAnnotationPage } from './AnnotationPage';
import { Audio, AudioComponentProps } from './Audio';
import React, { ReactNode } from 'react';
import { BoxStyle } from '@atlas-viewer/atlas';
import { StrategyActions } from '../../hooks/useRenderingStrategy';
import { AudioComponentProps } from './Audio';
import {
ComplexTimelineStrategy,
EmptyStrategy,
MediaStrategy,
RenderingStrategy,
} from '../../features/rendering-strategy/strategies';
import { Video, VideoComponentProps } from './Video';
import { Model } from './Model';
import { CanvasContext } from '../../context/CanvasContext';
import { VideoComponentProps } from './Video';
import { SingleImageStrategy } from '../../features/rendering-strategy/image-strategy';
import { CanvasBackground } from './CanvasBackground';
import { ImageWithOptionalService } from '../../features/rendering-strategy/resource-types';
import { PlaceholderCanvas } from './PlaceholderCanvas';
import { LocaleString } from '../../utility/i18n-utils';
import { useOverlay } from '../context/overlays';
import { useViewerPreset, ViewerPresetContext } from '../../context/ViewerPresetContext';
import { ChoiceDescription } from '@iiif/helpers';
import { useWorldSize } from '../context/world-size';
import { VideoYouTube } from './VideoYouTube';
import { RenderComplexTimeline } from './ComplexTimeline';
import { RenderTextualContent } from './TextualContent';
import { CanvasStrategyProvider } from './CanvasStrategyProvider';
import { CanvasWorldObject } from './CanvasWorldObject';
import { RenderEmptyStrategy } from '../strategy/EmptyStrategy';
import { RenderComplexTimelineStrategy } from '../strategy/ComplexTimelineStrategy';
import { RenderTextualContentStrategy } from '../strategy/TextualContentStrategy';
import { RenderImageStrategy } from '../strategy/ImageStrategy';
import { Render3DModelStrategy } from '../strategy/3dModelStrategy';
import { RenderAnnotationStrategy } from '../strategy/AnnotationStrategy';
import { RenderAudioStrategy } from '../strategy/AudioStrategy';
import { RenderAccompanyingCanvas } from '../strategy/AccompanyingCanvas';
import { RenderVideoStrategy } from '../strategy/VideoStrategy';
import { RenderYouTubeStrategy } from '../strategy/YouTubeStrategy';

export type CanvasProps = {
x?: number;
Expand All @@ -46,6 +39,7 @@ export type CanvasProps = {
viewControlsDeps?: any[];
renderMediaControls?: (strategy: MediaStrategy) => ReactNode;
renderComplexTimelineControls?: (strategy: ComplexTimelineStrategy) => ReactNode;
complexTimelineControlsDeps?: any[];
mediaControlsDeps?: any[];
strategies?: Array<RenderingStrategy['type']>;
backgroundStyle?: BoxStyle;
Expand All @@ -71,6 +65,7 @@ export function RenderCanvas({
renderViewerControls,
renderMediaControls,
renderComplexTimelineControls,
complexTimelineControlsDeps,
viewControlsDeps,
mediaControlsDeps,
strategies,
Expand All @@ -84,223 +79,37 @@ export function RenderCanvas({
components = {},
children,
}: CanvasProps) {
const canvas = useCanvas();
const elementProps = useResourceEvents(canvas, ['deep-zoom']);
const [virtualPage] = useVirtualAnnotationPageContext();
const preset = useViewerPreset();
const vault = useVault();
const helper = useMemo(() => createStylesHelper(vault), [vault]);
const [strategy, actions] = useRenderingStrategy({
strategies: strategies || ['images'],
defaultChoices: defaultChoices?.map(({ id }) => id),
});
const choice = strategy.type === 'images' ? strategy.choice : undefined;
const bestScale = useMemo(() => {
if (keepCanvasScale) {
return 1;
}
return Math.max(
1,
...(strategy.type === 'images'
? strategy.images.map((i) => {
return (i.width || 0) / i.target?.spatial.width;
})
: [])
);
}, [keepCanvasScale, strategy]);

useWorldSize(bestScale);

useEffect(() => {
if (registerActions) {
registerActions(actions);
}
}, [strategy.annotations]);

useEffect(() => {
if (defaultChoices) {
for (const choice of defaultChoices) {
if (typeof choice.opacity !== 'undefined') {
helper.applyStyles({ id: choice.id }, 'atlas', {
opacity: choice.opacity,
});
}
}
}
}, [defaultChoices]);

useLayoutEffect(() => {
if (onChoiceChange) {
onChoiceChange(choice);
}
}, [choice]);

useOverlay(
preset &&
(strategy.type === 'images' ||
strategy.type === 'empty' ||
(strategy.type === 'textual-content' && renderViewerControls))
? 'overlay'
: 'none',
`canvas-portal-controls-${canvas?.id}`,
ViewerPresetContext.Provider,
renderViewerControls
? {
value: preset || null,
children: renderViewerControls(strategy as any),
}
: {},
[canvas, preset, strategy, ...(viewControlsDeps || [])]
);

const thumbnail = useThumbnail({ maxWidth: 256, maxHeight: 256 });

if (!canvas) {
return null;
}

// accompanyingCanvas
const accompanyingCanvas = canvas.accompanyingCanvas;
const placeholderCanvas = canvas.placeholderCanvas;

const thumbnailFallbackImage =
thumbnail && thumbnail.type === 'fixed' ? (
<world-object height={canvas.height} width={canvas.width} x={x} y={y}>
<world-image
uri={thumbnail.id}
target={{ x: 0, y: 0, width: canvas.width, height: canvas.height }}
display={
thumbnail.width && thumbnail.height
? {
width: thumbnail.width,
height: thumbnail.height,
}
: undefined
}
crop={undefined}
/>
</world-object>
) : null;

if (strategy.type === 'unknown') {
if (thumbnailFallbackImage) {
return thumbnailFallbackImage;
}

if (throwOnUnknown) {
throw new Error(strategy.reason || 'Unknown image strategy');
}

return null;
}

const annotations = (
<Fragment>
{virtualPage ? <RenderAnnotationPage page={virtualPage} /> : null}
{strategy.annotations && strategy.annotations.pages
? strategy.annotations.pages.map((page) => {
return <RenderAnnotationPage key={page.id} page={page} />;
})
: null}
{children}
</Fragment>
);

const totalKey = strategy.type === 'images' ? strategy.images.length : 0;

const renderPlaceholderCanvas =
strategy.type === 'media' && strategy.media.type === 'Video' && placeholderCanvas ? (
<PlaceholderCanvas renderViewerControls={renderViewerControls} />
) : null;

return (
<>
<world-object
key={`${canvas.id}/${strategy.type}/${totalKey}`}
height={canvas.height}
width={canvas.width}
// scale={bestScale}
x={x}
y={y}
{...elementProps}
>
{strategy.type === 'empty' || alwaysShowBackground ? <CanvasBackground style={backgroundStyle} /> : null}
{strategy.type === 'complex-timeline' ? (
<RenderComplexTimeline strategy={strategy}>
{renderComplexTimelineControls ? renderComplexTimelineControls(strategy) : null}
</RenderComplexTimeline>
) : null}
{strategy.type === 'textual-content' ? (
<>
<RenderTextualContent strategy={strategy} onClickPaintingAnnotation={onClickPaintingAnnotation} />
{annotations}
</>
) : null}
{strategy.type === 'images' ? (
<>
{strategy.images.map((image, idx) => (
<RenderImage
isStatic={isStatic}
key={image.id + idx}
image={image}
id={image.id}
thumbnail={idx === 0 ? thumbnail : undefined}
selector={image.selector}
enableSizes={enableSizes}
onClick={
onClickPaintingAnnotation
? (e) => {
onClickPaintingAnnotation(image.annotationId, image, e);
}
: undefined
}
/>
))}
{annotations}
</>
) : null}
{strategy.type === '3d-model' ? <Model model={strategy.model} /> : null}
{strategy.type === 'media' ? (
<>
{strategy.media.type === 'Sound' ? (
<Audio media={strategy.media} mediaControlsDeps={mediaControlsDeps} audioCopmonent={components.Audio}>
{thumbnailFallbackImage}
{renderMediaControls ? renderMediaControls(strategy) : null}
</Audio>
) : strategy.media.type === 'Video' ? (
<Video
captions={strategy.captions}
media={strategy.media}
mediaControlsDeps={mediaControlsDeps}
videoComponent={components.Video}
>
{thumbnailFallbackImage}
{renderMediaControls ? renderMediaControls(strategy) : null}
</Video>
) : strategy.media.type === 'VideoYouTube' && enableYouTube ? (
<VideoYouTube media={strategy.media} mediaControlsDeps={mediaControlsDeps}>
{thumbnailFallbackImage}
{renderMediaControls ? renderMediaControls(strategy) : null}
</VideoYouTube>
) : null}
</>
) : null}
{/* This is required to fix a race condition. */}
</world-object>

{/* Accompanying canvas if its available */}
{strategy.type === 'media' && strategy.media.type === 'Sound' && accompanyingCanvas ? (
<CanvasContext canvas={accompanyingCanvas.id}>
<RenderCanvas renderViewerControls={renderViewerControls} />
</CanvasContext>
) : null}

{/* Fallback to placeholder canvas, we don't currently have a way to know if the audio is playing at this level. */}
{strategy.type === 'media' && strategy.media.type === 'Sound' && placeholderCanvas && !accompanyingCanvas ? (
<CanvasContext canvas={placeholderCanvas.id}>
<RenderCanvas renderViewerControls={renderViewerControls} />
</CanvasContext>
) : null}
</>
<CanvasStrategyProvider
throwOnUnknown={throwOnUnknown}
onChoiceChange={onChoiceChange}
registerActions={registerActions}
strategies={strategies}
defaultChoices={defaultChoices}
mediaControlsDeps={mediaControlsDeps}
renderMediaControls={renderMediaControls}
renderViewerControls={renderViewerControls}
renderComplexTimelineControls={renderComplexTimelineControls}
complexTimelineControlsDeps={complexTimelineControlsDeps}
viewControlsDeps={viewControlsDeps}
>
<CanvasWorldObject keepCanvasScale={keepCanvasScale} x={x} y={y}>
<RenderEmptyStrategy alwaysShowBackground={alwaysShowBackground} backgroundStyle={backgroundStyle} />
<RenderComplexTimelineStrategy />
<RenderTextualContentStrategy />
<RenderImageStrategy
isStatic={isStatic}
enableSizes={enableSizes}
onClickPaintingAnnotation={onClickPaintingAnnotation}
/>
<RenderAnnotationStrategy />
<Render3DModelStrategy />
<RenderAudioStrategy as={components.Audio} />
<RenderVideoStrategy as={components.Video} />
{enableYouTube ? <RenderYouTubeStrategy /> : null}
{children}
</CanvasWorldObject>
<RenderAccompanyingCanvas />
</CanvasStrategyProvider>
);
}
Loading

0 comments on commit 2c6354d

Please sign in to comment.