diff --git a/src/Viewer.tsx b/src/Viewer.tsx index 742014100..41efda0c2 100644 --- a/src/Viewer.tsx +++ b/src/Viewer.tsx @@ -11,7 +11,7 @@ import { NotificationConfig } from "antd/es/notification/interface"; import React, { ReactElement, useCallback, useContext, useEffect, useMemo, useReducer, useRef, useState } from "react"; import { Link, Location, useLocation, useSearchParams } from "react-router-dom"; -import { ColorizeCanvas, Dataset, Track } from "./colorizer"; +import { Dataset, Track } from "./colorizer"; import { DEFAULT_CATEGORICAL_PALETTE_KEY, DISPLAY_CATEGORICAL_PALETTE_KEYS, @@ -37,6 +37,7 @@ import { DEFAULT_PLAYBACK_FPS } from "./constants"; import { FlexRow, FlexRowAlignCenter } from "./styles/utils"; import { LocationState } from "./types"; +import CanvasWithOverlay from "./colorizer/CanvasWithOverlay"; import Collection from "./colorizer/Collection"; import { BACKGROUND_ID } from "./colorizer/ColorizeCanvas"; import { FeatureType } from "./colorizer/Dataset"; @@ -73,7 +74,7 @@ function Viewer(): ReactElement { const [, startTransition] = React.useTransition(); const canv = useConstructor(() => { - const canvas = new ColorizeCanvas(); + const canvas = new CanvasWithOverlay(); canvas.domElement.className = styles.colorizeCanvas; return canvas; }); diff --git a/src/colorizer/CanvasUIOverlay.ts b/src/colorizer/CanvasWithOverlay.ts similarity index 68% rename from src/colorizer/CanvasUIOverlay.ts rename to src/colorizer/CanvasWithOverlay.ts index f3966c03d..131df93ab 100644 --- a/src/colorizer/CanvasUIOverlay.ts +++ b/src/colorizer/CanvasWithOverlay.ts @@ -2,6 +2,8 @@ import { Vector2 } from "three"; import { numberToSciNotation } from "./utils/math_utils"; +import ColorizeCanvas from "./ColorizeCanvas"; + type StyleOptions = { fontSizePx: number; fontFamily: string; @@ -12,16 +14,10 @@ type StyleOptions = { type ScaleBarOptions = StyleOptions & { minWidthPx: number; visible: boolean; - unitsPerScreenPixel: number; - units: string; }; type TimestampOptions = StyleOptions & { visible: boolean; - maxTimeSec: number; - currTimeSec: number; - frameDurationSec: number; - startTimeSec: number; }; type OverlayFillOptions = { @@ -42,18 +38,12 @@ const defaultStyleOptions: StyleOptions = { const defaultScaleBarOptions: ScaleBarOptions = { ...defaultStyleOptions, minWidthPx: 80, - visible: false, - unitsPerScreenPixel: 1, - units: "", + visible: true, }; const defaultTimestampOptions: TimestampOptions = { ...defaultStyleOptions, - visible: false, - frameDurationSec: 1, - startTimeSec: 0, - maxTimeSec: 1, - currTimeSec: 0, + visible: true, }; const defaultBackgroundOptions: OverlayFillOptions = { @@ -71,12 +61,17 @@ type RenderInfo = { render: () => void; }; +const EMPTY_RENDER_INFO: RenderInfo = { sizePx: new Vector2(0, 0), render: () => {} }; + /** - * A canvas used for drawing UI overlays over another screen region. (intended for use - * with `ColorizeCanvas`.) + * Extends the ColorizeCanvas class by overlaying and compositing additional + * dynamic elements (like a scale bar, timestamp, etc.) on top of the + * base rendered image. */ -export default class CanvasOverlay { - public readonly canvas: OffscreenCanvas; +export default class CanvasWithOverlay extends ColorizeCanvas { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + private scaleBarOptions: ScaleBarOptions; private timestampOptions: TimestampOptions; private backgroundOptions: OverlayFillOptions; @@ -88,20 +83,41 @@ export default class CanvasOverlay { timestampOptions: TimestampOptions = defaultTimestampOptions, overlayOptions: OverlayFillOptions = defaultBackgroundOptions ) { - this.canvas = new OffscreenCanvas(1, 1); + super(); + + this.canvas = document.createElement("canvas"); + this.canvas.style.display = "block"; + this.scaleBarOptions = scaleBarOptions; this.timestampOptions = timestampOptions; this.backgroundOptions = overlayOptions; this.canvasWidth = 1; this.canvasHeight = 1; + + const canvasContext = this.canvas.getContext("2d") as CanvasRenderingContext2D; + if (canvasContext === null) { + throw new Error("CanvasWithOverlay: Could not get canvas context; canvas.getContext('2d') returned null."); + } + this.ctx = canvasContext; } - /** - * Set the size of the canvas overlay. - */ - setSize(width: number, height: number): void { + // Wrapped ColorizeCanvas functions /////// + + get domElement(): HTMLCanvasElement { + // Override base ColorizeCanvas getter with the composited canvas. + return this.canvas; + } + + public setSize(width: number, height: number): void { this.canvasWidth = width; this.canvasHeight = height; + super.setSize(width, height); + } + + // Rendering //////////////////////////////// + + private getPixelRatio(): number { + return window.devicePixelRatio || 1; } updateScaleBarOptions(options: Partial): void { @@ -116,7 +132,7 @@ export default class CanvasOverlay { this.backgroundOptions = { ...this.backgroundOptions, ...options }; } - private getTextDimensions(ctx: OffscreenCanvasRenderingContext2D, text: string, options: StyleOptions): Vector2 { + private getTextDimensions(ctx: CanvasRenderingContext2D, text: string, options: StyleOptions): Vector2 { ctx.font = `${options.fontSizePx}px ${options.fontFamily}`; ctx.fillStyle = options.fontColor; const textWidth = ctx.measureText(text).width; @@ -132,7 +148,7 @@ export default class CanvasOverlay { * @returns the width and height of the text, as a Vector2. */ private renderRightAlignedText( - ctx: OffscreenCanvasRenderingContext2D, + ctx: CanvasRenderingContext2D, originPx: Vector2, text: string, options: StyleOptions @@ -167,13 +183,17 @@ export default class CanvasOverlay { * Unit widths will always have values `nx10^m`, where `n` is 1, 2, or 5, and `m` is an integer. Pixel widths * will always be greater than or equal to the `scaleBarOptions.minWidthPx`. * @param scaleBarOptions Configuration for the scale bar + * @param unitsPerScreenPixel The number of units per pixel on the screen. * @returns An object, containing keys for the width in pixels and units. */ - private static getScaleBarWidth(scaleBarOptions: ScaleBarOptions): { + private static getScaleBarWidth( + scaleBarOptions: ScaleBarOptions, + unitsPerScreenPixel: number + ): { scaleBarWidthPx: number; scaleBarWidthInUnits: number; } { - const minWidthUnits = scaleBarOptions.minWidthPx * scaleBarOptions.unitsPerScreenPixel; + const minWidthUnits = scaleBarOptions.minWidthPx * unitsPerScreenPixel; // Here we get the power of the most significant digit (MSD) of the minimum width converted to units. const msdPower = Math.ceil(Math.log10(minWidthUnits)); @@ -189,7 +209,7 @@ export default class CanvasOverlay { const scaleBarWidthInUnits = nextIncrement * 10 ** (msdPower - 1); // Convert back into pixels for rendering. // Cheat very slightly by rounding to the nearest pixel for cleaner rendering. - const scaleBarWidthPx = Math.round(scaleBarWidthInUnits / scaleBarOptions.unitsPerScreenPixel); + const scaleBarWidthPx = Math.round(scaleBarWidthInUnits / unitsPerScreenPixel); return { scaleBarWidthPx, scaleBarWidthInUnits }; } @@ -200,14 +220,23 @@ export default class CanvasOverlay { * - `size`: a vector representing the width and height of the rendered scale bar, in pixels. * - `render`: a callback that renders the scale bar to the canvas. */ - private getScaleBarRenderer(ctx: OffscreenCanvasRenderingContext2D, originPx: Vector2): RenderInfo { - if (!this.scaleBarOptions.unitsPerScreenPixel || !this.scaleBarOptions.visible) { - return { sizePx: new Vector2(0, 0), render: () => {} }; + private getScaleBarRenderer(originPx: Vector2): RenderInfo { + const frameDims = this.dataset?.metadata.frameDims; + const hasFrameDims = frameDims && frameDims.width !== 0 && frameDims.height !== 0; + + if (!hasFrameDims || !this.scaleBarOptions.visible) { + return EMPTY_RENDER_INFO; } + const canvasWidthInUnits = frameDims.width / this.frameSizeInCanvasCoordinates.x; + const unitsPerScreenPixel = canvasWidthInUnits / this.canvasWidth / this.getPixelRatio(); + ///////// Get scale bar width and unit label ///////// - const { scaleBarWidthPx, scaleBarWidthInUnits } = CanvasOverlay.getScaleBarWidth(this.scaleBarOptions); - const textContent = `${CanvasOverlay.formatScaleBarValue(scaleBarWidthInUnits)} ${this.scaleBarOptions.units}`; + const { scaleBarWidthPx, scaleBarWidthInUnits } = CanvasWithOverlay.getScaleBarWidth( + this.scaleBarOptions, + unitsPerScreenPixel + ); + const textContent = `${CanvasWithOverlay.formatScaleBarValue(scaleBarWidthInUnits)} ${frameDims.units}`; ///////// Calculate the padding and origins for drawing and size ///////// const scaleBarHeight = 10; @@ -217,15 +246,15 @@ export default class CanvasOverlay { const renderScaleBar = (): void => { // Render the scale bar - ctx.beginPath(); - ctx.strokeStyle = this.scaleBarOptions.fontColor; - ctx.lineWidth = 1; + this.ctx.beginPath(); + this.ctx.strokeStyle = this.scaleBarOptions.fontColor; + this.ctx.lineWidth = 1; // Draw, starting from the top right corner and going clockwise. - ctx.moveTo(scaleBarX, scaleBarY - scaleBarHeight); - ctx.lineTo(scaleBarX, scaleBarY); - ctx.lineTo(scaleBarX - scaleBarWidthPx, scaleBarY); - ctx.lineTo(scaleBarX - scaleBarWidthPx, scaleBarY - scaleBarHeight); - ctx.stroke(); + this.ctx.moveTo(scaleBarX, scaleBarY - scaleBarHeight); + this.ctx.lineTo(scaleBarX, scaleBarY); + this.ctx.lineTo(scaleBarX - scaleBarWidthPx, scaleBarY); + this.ctx.lineTo(scaleBarX - scaleBarWidthPx, scaleBarY - scaleBarHeight); + this.ctx.stroke(); }; // TODO: This looks bad at high magnification. A workaround would be to use CSS2DRenderer to @@ -234,7 +263,7 @@ export default class CanvasOverlay { const textPaddingPx = new Vector2(6, 4); const textOriginPx = new Vector2(originPx.x + textPaddingPx.x, originPx.y + textPaddingPx.y); const renderScaleBarText = (): void => { - this.renderRightAlignedText(ctx, textOriginPx, textContent, this.scaleBarOptions); + this.renderRightAlignedText(this.ctx, textOriginPx, textContent, this.scaleBarOptions); }; return { @@ -262,28 +291,37 @@ export default class CanvasOverlay { * - `ss (s)` * - `ss.sss (s)`. */ - private static getTimestampLabel(timestampOptions: TimestampOptions): string { - const useHours = timestampOptions.maxTimeSec >= 60 * 60; - const useMinutes = timestampOptions.maxTimeSec >= 60; + private getTimestampLabel(): string | undefined { + if (!this.dataset || !this.dataset.metadata.frameDurationSeconds) { + return undefined; + } + + const frameDurationSec = this.dataset.metadata.frameDurationSeconds; + const startTimeSec = this.dataset.metadata.startTimeSeconds; + const currTimeSec = this.getCurrentFrame() * frameDurationSec + startTimeSec; + const maxTimeSec = this.dataset.numberOfFrames * frameDurationSec + startTimeSec; + + const useHours = maxTimeSec >= 60 * 60; + const useMinutes = maxTimeSec >= 60; // Ignore seconds if the frame duration is in minute increments AND the start time is also in minute increments. - const useSeconds = !(timestampOptions.frameDurationSec % 60 === 0 && timestampOptions.startTimeSec % 60 === 0); + const useSeconds = !(frameDurationSec % 60 === 0 && startTimeSec % 60 === 0); const timestampDigits: string[] = []; const timestampUnits: string[] = []; if (useHours) { - const hours = Math.floor(timestampOptions.currTimeSec / (60 * 60)); + const hours = Math.floor(currTimeSec / (60 * 60)); timestampDigits.push(hours.toString().padStart(2, "0")); timestampUnits.push("h"); } if (useMinutes) { - const minutes = Math.floor(timestampOptions.currTimeSec / 60) % 60; + const minutes = Math.floor(currTimeSec / 60) % 60; timestampDigits.push(minutes.toString().padStart(2, "0")); timestampUnits.push("m"); } if (useSeconds) { - const seconds = timestampOptions.currTimeSec % 60; - if (!useHours && timestampOptions.frameDurationSec % 1.0 !== 0) { + const seconds = currTimeSec % 60; + if (!useHours && frameDurationSec % 1.0 !== 0) { // Duration increment is smaller than a second and we're not showing hours, so show milliseconds. timestampDigits.push(seconds.toFixed(3).padStart(6, "0")); } else { @@ -302,25 +340,28 @@ export default class CanvasOverlay { * - `size`: a vector representing the width and height of the rendered scale bar, in pixels. * - `render`: a callback that renders the scale bar to the canvas. */ - private getTimestampRenderer(ctx: OffscreenCanvasRenderingContext2D, originPx: Vector2): RenderInfo { + private getTimestampRenderer(originPx: Vector2): RenderInfo { if (!this.timestampOptions.visible) { return { sizePx: new Vector2(0, 0), render: () => {} }; } ////////////////// Format timestamp as text ////////////////// - const timestampFormatted = CanvasOverlay.getTimestampLabel(this.timestampOptions); + const timestampFormatted = this.getTimestampLabel(); + if (!timestampFormatted) { + return EMPTY_RENDER_INFO; + } // TODO: Would be nice to configure top/bottom/left/right padding separately. const timestampPaddingPx = new Vector2(6, 2); const timestampOriginPx = new Vector2(originPx.x + timestampPaddingPx.x, originPx.y + timestampPaddingPx.y); // Save the render function for later. const render = (): void => { - this.renderRightAlignedText(ctx, timestampOriginPx, timestampFormatted, this.scaleBarOptions); + this.renderRightAlignedText(this.ctx, timestampOriginPx, timestampFormatted, this.scaleBarOptions); }; return { sizePx: new Vector2( - timestampPaddingPx.x * 2 + this.getTextDimensions(ctx, timestampFormatted, this.timestampOptions).x, + timestampPaddingPx.x * 2 + this.getTextDimensions(this.ctx, timestampFormatted, this.timestampOptions).x, timestampPaddingPx.y * 2 + this.timestampOptions.fontSizePx ), render, @@ -333,48 +374,44 @@ export default class CanvasOverlay { * @param size Size of the background overlay. * @param options Configuration for the background overlay. */ - private static renderBackground( - ctx: OffscreenCanvasRenderingContext2D, - size: Vector2, - options: OverlayFillOptions - ): void { - ctx.fillStyle = options.fill; - ctx.strokeStyle = options.stroke; - ctx.beginPath(); - ctx.roundRect( - Math.round(ctx.canvas.width - size.x - options.marginPx.x) + 0.5, - Math.round(ctx.canvas.height - size.y - options.marginPx.y) + 0.5, + private renderBackground(size: Vector2, options: OverlayFillOptions): void { + this.ctx.fillStyle = options.fill; + this.ctx.strokeStyle = options.stroke; + this.ctx.beginPath(); + this.ctx.roundRect( + Math.round(this.ctx.canvas.width - size.x - options.marginPx.x) + 0.5, + Math.round(this.ctx.canvas.height - size.y - options.marginPx.y) + 0.5, Math.round(size.x), Math.round(size.y), options.radiusPx ); - ctx.fill(); - ctx.stroke(); - ctx.closePath(); + this.ctx.fill(); + this.ctx.stroke(); + this.ctx.closePath(); } /** * Render the overlay to the canvas. */ render(): void { - const ctx = this.canvas.getContext("2d") as OffscreenCanvasRenderingContext2D | null; - if (ctx === null) { - console.error("Could not get canvas context"); - return; - } - - const devicePixelRatio = window.devicePixelRatio || 1; + const devicePixelRatio = this.getPixelRatio(); this.canvas.width = this.canvasWidth * devicePixelRatio; this.canvas.height = this.canvasHeight * devicePixelRatio; //Clear canvas - ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // Because CanvasWithOverlay is a child of ColorizeCanvas, this renders the base + // colorized viewport image. It is then composited into the CanvasWithOverlay's canvas. + super.render(); + this.ctx.imageSmoothingEnabled = false; + this.ctx.drawImage(super.domElement, 0, 0); // Get dimensions + render methods for the elements, but don't render yet so we can draw the background // behind them. const origin = this.backgroundOptions.marginPx.clone().add(this.backgroundOptions.paddingPx); - const { sizePx: scaleBarDimensions, render: renderScaleBar } = this.getScaleBarRenderer(ctx, origin); + const { sizePx: scaleBarDimensions, render: renderScaleBar } = this.getScaleBarRenderer(origin); origin.y += scaleBarDimensions.y; - const { sizePx: timestampDimensions, render: renderTimestamp } = this.getTimestampRenderer(ctx, origin); + const { sizePx: timestampDimensions, render: renderTimestamp } = this.getTimestampRenderer(origin); // If both elements are invisible, don't render the background. if (scaleBarDimensions.equals(new Vector2(0, 0)) && timestampDimensions.equals(new Vector2(0, 0))) { @@ -387,7 +424,7 @@ export default class CanvasOverlay { scaleBarDimensions.y + timestampDimensions.y ); const boxSize = contentSize.clone().add(this.backgroundOptions.paddingPx.clone().multiplyScalar(2.0)); - CanvasOverlay.renderBackground(ctx, boxSize, this.backgroundOptions); + this.renderBackground(boxSize, this.backgroundOptions); // Draw elements over the background renderScaleBar(); diff --git a/src/colorizer/ColorizeCanvas.ts b/src/colorizer/ColorizeCanvas.ts index 3013c5fd5..ae4254f92 100644 --- a/src/colorizer/ColorizeCanvas.ts +++ b/src/colorizer/ColorizeCanvas.ts @@ -1,7 +1,6 @@ import { BufferAttribute, BufferGeometry, - CanvasTexture, Color, DataTexture, GLSL3, @@ -26,7 +25,6 @@ import { MAX_FEATURE_CATEGORIES } from "../constants"; import { DrawMode, FeatureDataType, OUT_OF_RANGE_COLOR_DEFAULT, OUTLIER_COLOR_DEFAULT } from "./types"; import { packDataTexture } from "./utils/texture_utils"; -import CanvasOverlay from "./CanvasUIOverlay"; import ColorRamp from "./ColorRamp"; import Dataset from "./Dataset"; import Track from "./Track"; @@ -117,16 +115,11 @@ export default class ColorizeCanvas { private mesh: Mesh; private pickMesh: Mesh; - /** UI overlay for scale bars, timestamps, and other information. */ - public overlay: CanvasOverlay; - // Rendered track line that shows the trajectory of a cell. private line: Line; private showTrackPath: boolean; - private showTimestamp: boolean; - private showScaleBar: boolean; - private frameSizeInCanvasCoordinates: Vector2; + protected frameSizeInCanvasCoordinates: Vector2; private frameToCanvasCoordinates: Vector2; /** @@ -148,17 +141,17 @@ export default class ColorizeCanvas { private renderer: WebGLRenderer; private pickRenderTarget: WebGLRenderTarget; - private dataset: Dataset | null; - private track: Track | null; + protected dataset: Dataset | null; + protected track: Track | null; private points: Float32Array; - private canvasResolution: Vector2 | null; - - private featureKey: string | null; - private selectedBackdropKey: string | null; - private colorRamp: ColorRamp; - private colorMapRangeMin: number; - private colorMapRangeMax: number; - private categoricalPalette: ColorRamp; + protected canvasResolution: Vector2 | null; + + protected featureKey: string | null; + protected selectedBackdropKey: string | null; + protected colorRamp: ColorRamp; + protected colorMapRangeMin: number; + protected colorMapRangeMax: number; + protected categoricalPalette: ColorRamp; private currentFrame: number; private onFrameChangeCallback: (isMissing: boolean) => void; @@ -226,9 +219,6 @@ export default class ColorizeCanvas { this.colorMapRangeMax = 0; this.currentFrame = 0; - this.overlay = new CanvasOverlay(); - this.showScaleBar = false; - this.showTimestamp = false; this.frameSizeInCanvasCoordinates = new Vector2(1, 1); this.frameToCanvasCoordinates = new Vector2(1, 1); this.zoomMultiplier = 1; @@ -256,7 +246,6 @@ export default class ColorizeCanvas { this.checkPixelRatio(); this.renderer.setSize(width, height); - this.overlay.setSize(width, height); // TODO: either make this a 1x1 target and draw it with a new camera every time we pick, // or keep it up to date with the canvas on each redraw (and don't draw to it when we pick!) this.pickRenderTarget.setSize(width, height); @@ -293,63 +282,6 @@ export default class ColorizeCanvas { this.render(); } - private updateScaleBar(): void { - // Update the scale bar units - const frameDims = this.dataset?.metadata.frameDims; - // Ignore cases where dimensions have size 0 - const hasFrameDims = frameDims && frameDims.width !== 0 && frameDims.height !== 0; - if (this.showScaleBar && hasFrameDims && this.canvasResolution !== null) { - // `frameDims` are already in the provided unit scaling, so we figure out the current - // size of the frame relative to the canvas to determine the canvas' width in units. - // We only consider X scaling here because the scale bar is always horizontal. - const canvasWidthInUnits = frameDims.width / this.frameSizeInCanvasCoordinates.x; - const unitsPerScreenPixel = canvasWidthInUnits / this.canvasResolution.x / this.renderer.getPixelRatio(); - this.overlay.updateScaleBarOptions({ unitsPerScreenPixel, units: frameDims.units, visible: true }); - } else { - this.overlay.updateScaleBarOptions({ visible: false }); - } - } - - setScaleBarVisibility(visible: boolean): void { - this.showScaleBar = visible; - this.updateScaleBar(); - this.overlay.render(); - } - - private updateTimestamp(): void { - // Calculate the current time stamp based on the current frame and the frame duration provided - // by the dataset (optionally, hide the timestamp if the frame duration is not provided). - // Pass along to the overlay as parameters. - if (this.showTimestamp && this.dataset) { - const frameDurationSec = this.dataset.metadata.frameDurationSeconds; - if (frameDurationSec) { - const startTimeSec = this.dataset.metadata.startTimeSeconds; - // Note: there's some semi-redundant information here, since the current timestamp and max - // timestamp could be calculated from the frame duration if we passed in the current + max - // frames instead. For now, it's ok to keep those calculations here in ColorizeCanvas so the - // overlay doesn't need to know frame numbers. The duration + start time are needed for - // time display calculations, however. - this.overlay.updateTimestampOptions({ - visible: true, - frameDurationSec, - startTimeSec, - currTimeSec: this.currentFrame * frameDurationSec + startTimeSec, - maxTimeSec: this.dataset.numberOfFrames * frameDurationSec + startTimeSec, - }); - return; - } - } - - // Hide the timestamp if configuration is invalid or it's disabled. - this.overlay.updateTimestampOptions({ visible: false }); - } - - setTimestampVisibility(visible: boolean): void { - this.showTimestamp = visible; - this.updateTimestamp(); - this.overlay.render(); - } - private updateScaling(frameResolution: Vector2 | null, canvasResolution: Vector2 | null): void { if (!frameResolution || !canvasResolution) { return; @@ -390,7 +322,6 @@ export default class ColorizeCanvas { 2 * this.panOffset.y * this.frameToCanvasCoordinates.y, 0 ); - this.updateScaleBar(); } public async setDataset(dataset: Dataset): Promise { @@ -698,15 +629,6 @@ export default class ColorizeCanvas { this.updateTrackRange(); this.updateRamp(); - // Overlay updates - this.updateScaleBar(); - this.updateTimestamp(); - - // Draw the overlay, and pass the resulting image as a texture to the shader. - this.overlay.render(); - const overlayTexture = new CanvasTexture(this.overlay.canvas); - this.setUniform("overlay", overlayTexture); - this.renderer.render(this.scene, this.camera); } diff --git a/src/colorizer/TimeControls.ts b/src/colorizer/TimeControls.ts index 89586be58..218a64e91 100644 --- a/src/colorizer/TimeControls.ts +++ b/src/colorizer/TimeControls.ts @@ -1,6 +1,6 @@ import { DEFAULT_PLAYBACK_FPS } from "../constants"; -import ColorizeCanvas from "./ColorizeCanvas"; +import CanvasWithOverlay from "./CanvasWithOverlay"; // time / playback controls const NO_TIMER_ID = -1; @@ -10,11 +10,11 @@ export default class TimeControls { private setFrameFn?: (frame: number) => Promise; private playbackFps: number; - private canvas: ColorizeCanvas; + private canvas: CanvasWithOverlay; private pauseCallbacks: (() => void)[]; - constructor(canvas: ColorizeCanvas, playbackFps: number = DEFAULT_PLAYBACK_FPS) { + constructor(canvas: CanvasWithOverlay, playbackFps: number = DEFAULT_PLAYBACK_FPS) { this.canvas = canvas; this.timerId = NO_TIMER_ID; this.pauseCallbacks = []; @@ -91,9 +91,6 @@ export default class TimeControls { * @param onNewFrameCallback An optional callback that will be called whenever a new frame is loaded. */ public async play(onNewFrameCallback: () => void = () => {}): Promise { - if (this.canvas.getCurrentFrame() >= this.canvas.getTotalFrames() - 1) { - await this.canvas.setFrame(0); - } this.playTimeSeries(onNewFrameCallback); } diff --git a/src/colorizer/index.ts b/src/colorizer/index.ts index 8b38d2297..6e396b842 100644 --- a/src/colorizer/index.ts +++ b/src/colorizer/index.ts @@ -1,4 +1,3 @@ -import ColorizeCanvas from "./ColorizeCanvas"; import ColorRamp, { ColorRampType } from "./ColorRamp"; import Dataset from "./Dataset"; import ImageFrameLoader from "./loaders/ImageFrameLoader"; @@ -8,16 +7,7 @@ import Track from "./Track"; export type { ArraySource, IArrayLoader, ITextureImageLoader } from "./loaders/ILoader"; -export { - ColorizeCanvas, - ColorRamp, - ColorRampType, - Dataset, - ImageFrameLoader, - UrlArrayLoader as JsonArrayLoader, - Plotting, - Track, -}; +export { ColorRamp, ColorRampType, Dataset, ImageFrameLoader, UrlArrayLoader as JsonArrayLoader, Plotting, Track }; export * from "./colors/categorical_palettes"; export * from "./colors/color_ramps"; diff --git a/src/components/CanvasWrapper.tsx b/src/components/CanvasWrapper.tsx index 2af6c9260..5c64e20f7 100644 --- a/src/components/CanvasWrapper.tsx +++ b/src/components/CanvasWrapper.tsx @@ -6,11 +6,12 @@ import { Color, ColorRepresentation, Vector2 } from "three"; import { clamp } from "three/src/math/MathUtils"; import { NoImageSVG } from "../assets"; -import { ColorizeCanvas, ColorRamp, Dataset, Track } from "../colorizer"; +import { ColorRamp, Dataset, Track } from "../colorizer"; import { ViewerConfig } from "../colorizer/types"; import * as mathUtils from "../colorizer/utils/math_utils"; import { FlexColumn, FlexColumnAlignCenter, VisuallyHidden } from "../styles/utils"; +import CanvasUIOverlay from "../colorizer/CanvasWithOverlay"; import Collection from "../colorizer/Collection"; import { AppThemeContext } from "./AppStyle"; import { AlertBannerProps } from "./Banner"; @@ -71,7 +72,7 @@ const MissingFileIconContainer = styled(FlexColumnAlignCenter)` `; type CanvasWrapperProps = { - canv: ColorizeCanvas; + canv: CanvasUIOverlay; /** Dataset to look up track and ID information in. * Changing this does NOT update the canvas dataset; do so * directly by calling `canv.setDataset()`. @@ -128,7 +129,7 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem const containerRef = useRef(null); const canv = props.canv; - const canvasRef = useRef(null); + const canvasPlaceholderRef = useRef(null); /** * Canvas zoom level, stored as its inverse. This makes it so linear changes in zoom level @@ -180,9 +181,9 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem canv.setOnFrameChangeCallback(onFrameChangedCallback); - // Mount the canvas to the wrapper's location in the document. + // Mount the canvas to the placeholder's location in the document. useEffect(() => { - canvasRef.current?.parentNode?.replaceChild(canv.domElement, canvasRef.current); + canvasPlaceholderRef.current?.parentNode?.replaceChild(canv.domElement, canvasPlaceholderRef.current); }, []); // These are all useMemo calls because the updates to the canvas must happen in the same render; @@ -196,9 +197,9 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem fontColor: theme.color.text.primary, fontFamily: theme.font.family, }; - canv.overlay.updateScaleBarOptions(defaultTheme); - canv.overlay.updateTimestampOptions(defaultTheme); - canv.overlay.updateBackgroundOptions({ stroke: theme.color.layout.borders }); + canv.updateScaleBarOptions(defaultTheme); + canv.updateTimestampOptions(defaultTheme); + canv.updateBackgroundOptions({ stroke: theme.color.layout.borders }); canv.setCanvasBackgroundColor(new Color(theme.color.viewport.background as ColorRepresentation)); }, [theme]); @@ -248,11 +249,11 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem // Update overlay settings useMemo(() => { - canv.setScaleBarVisibility(props.config.showScaleBar); + canv.updateScaleBarOptions({ visible: props.config.showScaleBar }); }, [props.config.showScaleBar]); useMemo(() => { - canv.setTimestampVisibility(props.config.showTimestamp); + canv.updateTimestampOptions({ visible: props.config.showTimestamp }); }, [props.config.showTimestamp]); // CANVAS RESIZING ///////////////////////////////////////////////// @@ -275,6 +276,7 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem const updateCanvasDimensions = (): void => { const canvasSizePx = getCanvasSizePx(); canv.setSize(canvasSizePx.x, canvasSizePx.y); + canv.setSize(canvasSizePx.x, canvasSizePx.y); }; updateCanvasDimensions(); // Initial size setting @@ -539,14 +541,12 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem -
+