From a53b4916a689f0ebe1b41b3271a1faf8be4f0528 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 19 Sep 2024 09:48:23 -0700 Subject: [PATCH 1/6] refactor: CanvasUIOverlay now extends ColorizeCanvas --- src/Viewer.tsx | 7 +- src/colorizer/CanvasUIOverlay.ts | 110 +++++++++++++++++++++------ src/colorizer/ColorizeCanvas.ts | 103 ++++--------------------- src/colorizer/IControllableCanvas.ts | 12 +++ src/components/CanvasWrapper.tsx | 41 ++++++---- 5 files changed, 145 insertions(+), 128 deletions(-) create mode 100644 src/colorizer/IControllableCanvas.ts diff --git a/src/Viewer.tsx b/src/Viewer.tsx index 742014100..02e99a5f8 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 CanvasOverlay from "./colorizer/CanvasUIOverlay"; import Collection from "./colorizer/Collection"; import { BACKGROUND_ID } from "./colorizer/ColorizeCanvas"; import { FeatureType } from "./colorizer/Dataset"; @@ -72,8 +73,9 @@ function Viewer(): ReactElement { const [, startTransition] = React.useTransition(); + const canvasHtmlRef = useRef(null); const canv = useConstructor(() => { - const canvas = new ColorizeCanvas(); + const canvas = new CanvasOverlay(); canvas.domElement.className = styles.colorizeCanvas; return canvas; }); @@ -952,6 +954,7 @@ function Viewer(): ReactElement { loading={isDatasetLoading} loadingProgress={datasetLoadProgress} canv={canv} + canvasRef={canvasHtmlRef} collection={collection || null} dataset={dataset} selectedBackdropKey={selectedBackdropKey} diff --git a/src/colorizer/CanvasUIOverlay.ts b/src/colorizer/CanvasUIOverlay.ts index f3966c03d..deba63646 100644 --- a/src/colorizer/CanvasUIOverlay.ts +++ b/src/colorizer/CanvasUIOverlay.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; @@ -42,14 +44,14 @@ const defaultStyleOptions: StyleOptions = { const defaultScaleBarOptions: ScaleBarOptions = { ...defaultStyleOptions, minWidthPx: 80, - visible: false, + visible: true, unitsPerScreenPixel: 1, units: "", }; const defaultTimestampOptions: TimestampOptions = { ...defaultStyleOptions, - visible: false, + visible: true, frameDurationSec: 1, startTimeSec: 0, maxTimeSec: 1, @@ -72,11 +74,12 @@ type RenderInfo = { }; /** - * A canvas used for drawing UI overlays over another screen region. (intended for use - * with `ColorizeCanvas`.) + * Composites overlays, headers, and footers on top of a ColorizeCanvas, rendering to an + * HTML canvas element. */ -export default class CanvasOverlay { - public readonly canvas: OffscreenCanvas; +export default class CanvasOverlay extends ColorizeCanvas { + private canvas: HTMLCanvasElement; + private scaleBarOptions: ScaleBarOptions; private timestampOptions: TimestampOptions; private backgroundOptions: OverlayFillOptions; @@ -88,7 +91,11 @@ 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; @@ -96,12 +103,67 @@ export default class CanvasOverlay { this.canvasHeight = 1; } - /** - * 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 getTotalFrames(): number { + return this.dataset ? this.dataset.numberOfFrames : 0; + } + + 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; + } + + 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.scaleBarOptions.visible && hasFrameDims) { + // `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.canvasWidth / this.getPixelRatio(); + this.updateScaleBarOptions({ unitsPerScreenPixel, units: frameDims.units, visible: true }); + } + } + + 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.timestampOptions.visible && 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.updateTimestampOptions({ + visible: true, + frameDurationSec, + startTimeSec, + currTimeSec: this.getCurrentFrame() * frameDurationSec + startTimeSec, + maxTimeSec: this.dataset.numberOfFrames * frameDurationSec + startTimeSec, + }); + return; + } + } } updateScaleBarOptions(options: Partial): void { @@ -116,7 +178,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 +194,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 @@ -200,7 +262,7 @@ 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 { + private getScaleBarRenderer(ctx: CanvasRenderingContext2D, originPx: Vector2): RenderInfo { if (!this.scaleBarOptions.unitsPerScreenPixel || !this.scaleBarOptions.visible) { return { sizePx: new Vector2(0, 0), render: () => {} }; } @@ -302,7 +364,7 @@ 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(ctx: CanvasRenderingContext2D, originPx: Vector2): RenderInfo { if (!this.timestampOptions.visible) { return { sizePx: new Vector2(0, 0), render: () => {} }; } @@ -333,11 +395,7 @@ 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 { + private static renderBackground(ctx: CanvasRenderingContext2D, size: Vector2, options: OverlayFillOptions): void { ctx.fillStyle = options.fill; ctx.strokeStyle = options.stroke; ctx.beginPath(); @@ -357,18 +415,26 @@ export default class CanvasOverlay { * Render the overlay to the canvas. */ render(): void { - const ctx = this.canvas.getContext("2d") as OffscreenCanvasRenderingContext2D | null; + const ctx = this.canvas.getContext("2d") as CanvasRenderingContext2D | 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); + // Render the viewport + super.render(); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(super.domElement, 0, 0); + + this.updateScaleBar(); + this.updateTimestamp(); + // 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); diff --git a/src/colorizer/ColorizeCanvas.ts b/src/colorizer/ColorizeCanvas.ts index 3013c5fd5..c12b43223 100644 --- a/src/colorizer/ColorizeCanvas.ts +++ b/src/colorizer/ColorizeCanvas.ts @@ -1,7 +1,6 @@ import { BufferAttribute, BufferGeometry, - CanvasTexture, Color, DataTexture, GLSL3, @@ -26,9 +25,9 @@ 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 { IControllableCanvas } from "./IControllableCanvas"; import Track from "./Track"; import pickFragmentShader from "./shaders/cellId_RGBA8U.frag"; @@ -110,23 +109,18 @@ const getDefaultUniforms = (): ColorizeUniforms => { }; }; -export default class ColorizeCanvas { +export default class ColorizeCanvas implements IControllableCanvas { private geometry: PlaneGeometry; private material: ShaderMaterial; private pickMaterial: ShaderMaterial; 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 +142,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 +220,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 +247,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 +283,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 +323,6 @@ export default class ColorizeCanvas { 2 * this.panOffset.y * this.frameToCanvasCoordinates.y, 0 ); - this.updateScaleBar(); } public async setDataset(dataset: Dataset): Promise { @@ -698,15 +630,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/IControllableCanvas.ts b/src/colorizer/IControllableCanvas.ts new file mode 100644 index 000000000..4422225f8 --- /dev/null +++ b/src/colorizer/IControllableCanvas.ts @@ -0,0 +1,12 @@ +import Dataset from "./Dataset"; + +export interface IControllableCanvas { + getTotalFrames: () => number; + getCurrentFrame: () => number; + setFrame: (frame: number) => Promise; + setFeatureKey: (key: string) => void; + setDataset: (dataset: Dataset) => void; + setSize: (width: number, height: number) => void; + render: () => void; + domElement: HTMLCanvasElement; +} diff --git a/src/components/CanvasWrapper.tsx b/src/components/CanvasWrapper.tsx index 2af6c9260..d60271989 100644 --- a/src/components/CanvasWrapper.tsx +++ b/src/components/CanvasWrapper.tsx @@ -1,16 +1,27 @@ import { HomeOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons"; import { Tooltip, TooltipProps } from "antd"; -import React, { ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import React, { + MutableRefObject, + ReactElement, + ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import styled from "styled-components"; 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/CanvasUIOverlay"; import Collection from "../colorizer/Collection"; import { AppThemeContext } from "./AppStyle"; import { AlertBannerProps } from "./Banner"; @@ -71,7 +82,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()`. @@ -105,6 +116,8 @@ type CanvasWrapperProps = { maxWidthPx?: number; maxHeightPx?: number; + + canvasRef: MutableRefObject; }; const defaultProps: Partial = { @@ -128,7 +141,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 +193,10 @@ 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); + props.canvasRef.current = canv.domElement; + 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 +210,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 +262,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 +289,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 +554,12 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem -
+
From f52e87b5e6b477a57be3cad92e5efa5bd6b41386 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 19 Sep 2024 13:40:23 -0700 Subject: [PATCH 2/6] refactor: Removed scale bar update functions, simplified CanvasOverlay --- src/Viewer.tsx | 2 - src/colorizer/CanvasUIOverlay.ts | 123 ++++++++++----------------- src/colorizer/ColorizeCanvas.ts | 3 +- src/colorizer/IControllableCanvas.ts | 12 --- src/components/CanvasWrapper.tsx | 15 +--- 5 files changed, 48 insertions(+), 107 deletions(-) delete mode 100644 src/colorizer/IControllableCanvas.ts diff --git a/src/Viewer.tsx b/src/Viewer.tsx index 02e99a5f8..d6946258d 100644 --- a/src/Viewer.tsx +++ b/src/Viewer.tsx @@ -73,7 +73,6 @@ function Viewer(): ReactElement { const [, startTransition] = React.useTransition(); - const canvasHtmlRef = useRef(null); const canv = useConstructor(() => { const canvas = new CanvasOverlay(); canvas.domElement.className = styles.colorizeCanvas; @@ -954,7 +953,6 @@ function Viewer(): ReactElement { loading={isDatasetLoading} loadingProgress={datasetLoadProgress} canv={canv} - canvasRef={canvasHtmlRef} collection={collection || null} dataset={dataset} selectedBackdropKey={selectedBackdropKey} diff --git a/src/colorizer/CanvasUIOverlay.ts b/src/colorizer/CanvasUIOverlay.ts index deba63646..15f3dad2a 100644 --- a/src/colorizer/CanvasUIOverlay.ts +++ b/src/colorizer/CanvasUIOverlay.ts @@ -14,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 = { @@ -45,17 +39,11 @@ const defaultScaleBarOptions: ScaleBarOptions = { ...defaultStyleOptions, minWidthPx: 80, visible: true, - unitsPerScreenPixel: 1, - units: "", }; const defaultTimestampOptions: TimestampOptions = { ...defaultStyleOptions, visible: true, - frameDurationSec: 1, - startTimeSec: 0, - maxTimeSec: 1, - currTimeSec: 0, }; const defaultBackgroundOptions: OverlayFillOptions = { @@ -73,9 +61,12 @@ type RenderInfo = { render: () => void; }; +const EMPTY_RENDER_INFO: RenderInfo = { sizePx: new Vector2(0, 0), render: () => {} }; + /** - * Composites overlays, headers, and footers on top of a ColorizeCanvas, rendering to an - * HTML canvas element. + * 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 extends ColorizeCanvas { private canvas: HTMLCanvasElement; @@ -110,10 +101,6 @@ export default class CanvasOverlay extends ColorizeCanvas { return this.canvas; } - public getTotalFrames(): number { - return this.dataset ? this.dataset.numberOfFrames : 0; - } - public setSize(width: number, height: number): void { this.canvasWidth = width; this.canvasHeight = height; @@ -126,46 +113,6 @@ export default class CanvasOverlay extends ColorizeCanvas { return window.devicePixelRatio || 1; } - 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.scaleBarOptions.visible && hasFrameDims) { - // `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.canvasWidth / this.getPixelRatio(); - this.updateScaleBarOptions({ unitsPerScreenPixel, units: frameDims.units, visible: true }); - } - } - - 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.timestampOptions.visible && 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.updateTimestampOptions({ - visible: true, - frameDurationSec, - startTimeSec, - currTimeSec: this.getCurrentFrame() * frameDurationSec + startTimeSec, - maxTimeSec: this.dataset.numberOfFrames * frameDurationSec + startTimeSec, - }); - return; - } - } - } - updateScaleBarOptions(options: Partial): void { this.scaleBarOptions = { ...this.scaleBarOptions, ...options }; } @@ -229,13 +176,17 @@ export default class CanvasOverlay extends ColorizeCanvas { * 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)); @@ -251,7 +202,7 @@ export default class CanvasOverlay extends ColorizeCanvas { 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 }; } @@ -263,13 +214,22 @@ export default class CanvasOverlay extends ColorizeCanvas { * - `render`: a callback that renders the scale bar to the canvas. */ private getScaleBarRenderer(ctx: CanvasRenderingContext2D, originPx: Vector2): RenderInfo { - if (!this.scaleBarOptions.unitsPerScreenPixel || !this.scaleBarOptions.visible) { - return { sizePx: new Vector2(0, 0), render: () => {} }; + 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 } = CanvasOverlay.getScaleBarWidth( + this.scaleBarOptions, + unitsPerScreenPixel + ); + const textContent = `${CanvasOverlay.formatScaleBarValue(scaleBarWidthInUnits)} ${frameDims.units}`; ///////// Calculate the padding and origins for drawing and size ///////// const scaleBarHeight = 10; @@ -324,28 +284,37 @@ export default class CanvasOverlay extends ColorizeCanvas { * - `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 { @@ -370,7 +339,10 @@ export default class CanvasOverlay extends ColorizeCanvas { } ////////////////// 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); @@ -432,9 +404,6 @@ export default class CanvasOverlay extends ColorizeCanvas { ctx.imageSmoothingEnabled = false; ctx.drawImage(super.domElement, 0, 0); - this.updateScaleBar(); - this.updateTimestamp(); - // 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); diff --git a/src/colorizer/ColorizeCanvas.ts b/src/colorizer/ColorizeCanvas.ts index c12b43223..ae4254f92 100644 --- a/src/colorizer/ColorizeCanvas.ts +++ b/src/colorizer/ColorizeCanvas.ts @@ -27,7 +27,6 @@ import { packDataTexture } from "./utils/texture_utils"; import ColorRamp from "./ColorRamp"; import Dataset from "./Dataset"; -import { IControllableCanvas } from "./IControllableCanvas"; import Track from "./Track"; import pickFragmentShader from "./shaders/cellId_RGBA8U.frag"; @@ -109,7 +108,7 @@ const getDefaultUniforms = (): ColorizeUniforms => { }; }; -export default class ColorizeCanvas implements IControllableCanvas { +export default class ColorizeCanvas { private geometry: PlaneGeometry; private material: ShaderMaterial; private pickMaterial: ShaderMaterial; diff --git a/src/colorizer/IControllableCanvas.ts b/src/colorizer/IControllableCanvas.ts deleted file mode 100644 index 4422225f8..000000000 --- a/src/colorizer/IControllableCanvas.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Dataset from "./Dataset"; - -export interface IControllableCanvas { - getTotalFrames: () => number; - getCurrentFrame: () => number; - setFrame: (frame: number) => Promise; - setFeatureKey: (key: string) => void; - setDataset: (dataset: Dataset) => void; - setSize: (width: number, height: number) => void; - render: () => void; - domElement: HTMLCanvasElement; -} diff --git a/src/components/CanvasWrapper.tsx b/src/components/CanvasWrapper.tsx index d60271989..3297efbff 100644 --- a/src/components/CanvasWrapper.tsx +++ b/src/components/CanvasWrapper.tsx @@ -1,16 +1,6 @@ import { HomeOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons"; import { Tooltip, TooltipProps } from "antd"; -import React, { - MutableRefObject, - ReactElement, - ReactNode, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { ReactElement, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import styled from "styled-components"; import { Color, ColorRepresentation, Vector2 } from "three"; import { clamp } from "three/src/math/MathUtils"; @@ -116,8 +106,6 @@ type CanvasWrapperProps = { maxWidthPx?: number; maxHeightPx?: number; - - canvasRef: MutableRefObject; }; const defaultProps: Partial = { @@ -195,7 +183,6 @@ export default function CanvasWrapper(inputProps: CanvasWrapperProps): ReactElem // Mount the canvas to the placeholder's location in the document. useEffect(() => { - props.canvasRef.current = canv.domElement; canvasPlaceholderRef.current?.parentNode?.replaceChild(canv.domElement, canvasPlaceholderRef.current); }, []); From 1f46f8e5e088a934b07e26e38daf2ab1fe3ba25b Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 24 Sep 2024 14:50:03 -0700 Subject: [PATCH 3/6] refactor: Renamed CanvasOverlay to `CanvasWithOverlay`, changed canvas context to a class property --- src/Viewer.tsx | 4 +- ...anvasUIOverlay.ts => CanvasWithOverlay.ts} | 77 ++++++++++--------- src/components/CanvasWrapper.tsx | 2 +- 3 files changed, 42 insertions(+), 41 deletions(-) rename src/colorizer/{CanvasUIOverlay.ts => CanvasWithOverlay.ts} (86%) diff --git a/src/Viewer.tsx b/src/Viewer.tsx index d6946258d..41efda0c2 100644 --- a/src/Viewer.tsx +++ b/src/Viewer.tsx @@ -37,7 +37,7 @@ import { DEFAULT_PLAYBACK_FPS } from "./constants"; import { FlexRow, FlexRowAlignCenter } from "./styles/utils"; import { LocationState } from "./types"; -import CanvasOverlay from "./colorizer/CanvasUIOverlay"; +import CanvasWithOverlay from "./colorizer/CanvasWithOverlay"; import Collection from "./colorizer/Collection"; import { BACKGROUND_ID } from "./colorizer/ColorizeCanvas"; import { FeatureType } from "./colorizer/Dataset"; @@ -74,7 +74,7 @@ function Viewer(): ReactElement { const [, startTransition] = React.useTransition(); const canv = useConstructor(() => { - const canvas = new CanvasOverlay(); + 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 86% rename from src/colorizer/CanvasUIOverlay.ts rename to src/colorizer/CanvasWithOverlay.ts index 15f3dad2a..3d0add2fc 100644 --- a/src/colorizer/CanvasUIOverlay.ts +++ b/src/colorizer/CanvasWithOverlay.ts @@ -68,8 +68,9 @@ const EMPTY_RENDER_INFO: RenderInfo = { sizePx: new Vector2(0, 0), render: () => * dynamic elements (like a scale bar, timestamp, etc.) on top of the * base rendered image. */ -export default class CanvasOverlay extends ColorizeCanvas { +export default class CanvasWithOverlay extends ColorizeCanvas { private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; private scaleBarOptions: ScaleBarOptions; private timestampOptions: TimestampOptions; @@ -92,6 +93,12 @@ export default class CanvasOverlay extends ColorizeCanvas { 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; } // Wrapped ColorizeCanvas functions /////// @@ -213,7 +220,7 @@ export default class CanvasOverlay extends ColorizeCanvas { * - `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: CanvasRenderingContext2D, originPx: Vector2): RenderInfo { + private getScaleBarRenderer(originPx: Vector2): RenderInfo { const frameDims = this.dataset?.metadata.frameDims; const hasFrameDims = frameDims && frameDims.width !== 0 && frameDims.height !== 0; @@ -225,11 +232,11 @@ export default class CanvasOverlay extends ColorizeCanvas { const unitsPerScreenPixel = canvasWidthInUnits / this.canvasWidth / this.getPixelRatio(); ///////// Get scale bar width and unit label ///////// - const { scaleBarWidthPx, scaleBarWidthInUnits } = CanvasOverlay.getScaleBarWidth( + const { scaleBarWidthPx, scaleBarWidthInUnits } = CanvasWithOverlay.getScaleBarWidth( this.scaleBarOptions, unitsPerScreenPixel ); - const textContent = `${CanvasOverlay.formatScaleBarValue(scaleBarWidthInUnits)} ${frameDims.units}`; + const textContent = `${CanvasWithOverlay.formatScaleBarValue(scaleBarWidthInUnits)} ${frameDims.units}`; ///////// Calculate the padding and origins for drawing and size ///////// const scaleBarHeight = 10; @@ -239,15 +246,15 @@ export default class CanvasOverlay extends ColorizeCanvas { 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 @@ -256,7 +263,7 @@ export default class CanvasOverlay extends ColorizeCanvas { 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 { @@ -333,7 +340,7 @@ export default class CanvasOverlay extends ColorizeCanvas { * - `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: CanvasRenderingContext2D, originPx: Vector2): RenderInfo { + private getTimestampRenderer(originPx: Vector2): RenderInfo { if (!this.timestampOptions.visible) { return { sizePx: new Vector2(0, 0), render: () => {} }; } @@ -349,12 +356,12 @@ export default class CanvasOverlay extends ColorizeCanvas { 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, @@ -367,49 +374,43 @@ export default class CanvasOverlay extends ColorizeCanvas { * @param size Size of the background overlay. * @param options Configuration for the background overlay. */ - private static renderBackground(ctx: CanvasRenderingContext2D, 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 CanvasRenderingContext2D | null; - if (ctx === null) { - console.error("Could not get canvas context"); - return; - } - 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); // Render the viewport super.render(); - ctx.imageSmoothingEnabled = false; - ctx.drawImage(super.domElement, 0, 0); + 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))) { @@ -422,7 +423,7 @@ export default class CanvasOverlay extends ColorizeCanvas { 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/components/CanvasWrapper.tsx b/src/components/CanvasWrapper.tsx index 3297efbff..5c64e20f7 100644 --- a/src/components/CanvasWrapper.tsx +++ b/src/components/CanvasWrapper.tsx @@ -11,7 +11,7 @@ import { ViewerConfig } from "../colorizer/types"; import * as mathUtils from "../colorizer/utils/math_utils"; import { FlexColumn, FlexColumnAlignCenter, VisuallyHidden } from "../styles/utils"; -import CanvasUIOverlay from "../colorizer/CanvasUIOverlay"; +import CanvasUIOverlay from "../colorizer/CanvasWithOverlay"; import Collection from "../colorizer/Collection"; import { AppThemeContext } from "./AppStyle"; import { AlertBannerProps } from "./Banner"; From 810ec5752634c52edd151e552c4a9522ae13e6d8 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Tue, 24 Sep 2024 15:06:16 -0700 Subject: [PATCH 4/6] refactor: Added controllable canvas interface for canvas overlay --- src/colorizer/CanvasWithOverlay.ts | 3 ++- src/colorizer/ColorizeCanvas.ts | 3 ++- src/colorizer/TimeControls.ts | 7 +++---- src/colorizer/canvas/IControllableCanvas.ts | 14 ++++++++++++++ src/colorizer/canvas/index.ts | 3 +++ src/colorizer/index.ts | 13 ++----------- 6 files changed, 26 insertions(+), 17 deletions(-) create mode 100644 src/colorizer/canvas/IControllableCanvas.ts create mode 100644 src/colorizer/canvas/index.ts diff --git a/src/colorizer/CanvasWithOverlay.ts b/src/colorizer/CanvasWithOverlay.ts index 3d0add2fc..896a13aed 100644 --- a/src/colorizer/CanvasWithOverlay.ts +++ b/src/colorizer/CanvasWithOverlay.ts @@ -2,6 +2,7 @@ import { Vector2 } from "three"; import { numberToSciNotation } from "./utils/math_utils"; +import { IControllableCanvas } from "./canvas/IControllableCanvas"; import ColorizeCanvas from "./ColorizeCanvas"; type StyleOptions = { @@ -68,7 +69,7 @@ const EMPTY_RENDER_INFO: RenderInfo = { sizePx: new Vector2(0, 0), render: () => * dynamic elements (like a scale bar, timestamp, etc.) on top of the * base rendered image. */ -export default class CanvasWithOverlay extends ColorizeCanvas { +export default class CanvasWithOverlay extends ColorizeCanvas implements IControllableCanvas { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; diff --git a/src/colorizer/ColorizeCanvas.ts b/src/colorizer/ColorizeCanvas.ts index ae4254f92..df6317b26 100644 --- a/src/colorizer/ColorizeCanvas.ts +++ b/src/colorizer/ColorizeCanvas.ts @@ -22,6 +22,7 @@ import { } from "three"; import { MAX_FEATURE_CATEGORIES } from "../constants"; +import { IControllableCanvas } from "./canvas"; import { DrawMode, FeatureDataType, OUT_OF_RANGE_COLOR_DEFAULT, OUTLIER_COLOR_DEFAULT } from "./types"; import { packDataTexture } from "./utils/texture_utils"; @@ -108,7 +109,7 @@ const getDefaultUniforms = (): ColorizeUniforms => { }; }; -export default class ColorizeCanvas { +export default class ColorizeCanvas implements IControllableCanvas { private geometry: PlaneGeometry; private material: ShaderMaterial; private pickMaterial: ShaderMaterial; diff --git a/src/colorizer/TimeControls.ts b/src/colorizer/TimeControls.ts index 89586be58..e82cc3af6 100644 --- a/src/colorizer/TimeControls.ts +++ b/src/colorizer/TimeControls.ts @@ -1,6 +1,5 @@ import { DEFAULT_PLAYBACK_FPS } from "../constants"; - -import ColorizeCanvas from "./ColorizeCanvas"; +import { IControllableCanvas } from "./canvas"; // time / playback controls const NO_TIMER_ID = -1; @@ -10,11 +9,11 @@ export default class TimeControls { private setFrameFn?: (frame: number) => Promise; private playbackFps: number; - private canvas: ColorizeCanvas; + private canvas: IControllableCanvas; private pauseCallbacks: (() => void)[]; - constructor(canvas: ColorizeCanvas, playbackFps: number = DEFAULT_PLAYBACK_FPS) { + constructor(canvas: IControllableCanvas, playbackFps: number = DEFAULT_PLAYBACK_FPS) { this.canvas = canvas; this.timerId = NO_TIMER_ID; this.pauseCallbacks = []; diff --git a/src/colorizer/canvas/IControllableCanvas.ts b/src/colorizer/canvas/IControllableCanvas.ts new file mode 100644 index 000000000..43849ee5a --- /dev/null +++ b/src/colorizer/canvas/IControllableCanvas.ts @@ -0,0 +1,14 @@ +import Dataset from "../Dataset"; + +export interface IControllableCanvas { + getTotalFrames(): number; + getCurrentFrame(): number; + setFrame(frame: number): Promise; + + render(): void; + + setDataset(dataset: Dataset): void; + setFeatureKey(key: string): void; + + domElement: HTMLCanvasElement; +} diff --git a/src/colorizer/canvas/index.ts b/src/colorizer/canvas/index.ts new file mode 100644 index 000000000..e44673a38 --- /dev/null +++ b/src/colorizer/canvas/index.ts @@ -0,0 +1,3 @@ +import { IControllableCanvas } from "./IControllableCanvas"; + +export type { IControllableCanvas }; diff --git a/src/colorizer/index.ts b/src/colorizer/index.ts index 8b38d2297..4d1a52a44 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"; @@ -6,18 +5,10 @@ import UrlArrayLoader from "./loaders/UrlArrayLoader"; import Plotting from "./Plotting"; import Track from "./Track"; +export type { IControllableCanvas } from "./canvas/IControllableCanvas"; 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"; From 9e03f865e12133216784e9773cca5a8a4401e505 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 26 Sep 2024 10:40:17 -0700 Subject: [PATCH 5/6] refactor: Deleted IControllableCanvas --- src/colorizer/CanvasWithOverlay.ts | 3 +-- src/colorizer/ColorizeCanvas.ts | 3 +-- src/colorizer/TimeControls.ts | 10 ++++------ src/colorizer/canvas/IControllableCanvas.ts | 14 -------------- src/colorizer/canvas/index.ts | 3 --- 5 files changed, 6 insertions(+), 27 deletions(-) delete mode 100644 src/colorizer/canvas/IControllableCanvas.ts delete mode 100644 src/colorizer/canvas/index.ts diff --git a/src/colorizer/CanvasWithOverlay.ts b/src/colorizer/CanvasWithOverlay.ts index 896a13aed..3d0add2fc 100644 --- a/src/colorizer/CanvasWithOverlay.ts +++ b/src/colorizer/CanvasWithOverlay.ts @@ -2,7 +2,6 @@ import { Vector2 } from "three"; import { numberToSciNotation } from "./utils/math_utils"; -import { IControllableCanvas } from "./canvas/IControllableCanvas"; import ColorizeCanvas from "./ColorizeCanvas"; type StyleOptions = { @@ -69,7 +68,7 @@ const EMPTY_RENDER_INFO: RenderInfo = { sizePx: new Vector2(0, 0), render: () => * dynamic elements (like a scale bar, timestamp, etc.) on top of the * base rendered image. */ -export default class CanvasWithOverlay extends ColorizeCanvas implements IControllableCanvas { +export default class CanvasWithOverlay extends ColorizeCanvas { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; diff --git a/src/colorizer/ColorizeCanvas.ts b/src/colorizer/ColorizeCanvas.ts index df6317b26..ae4254f92 100644 --- a/src/colorizer/ColorizeCanvas.ts +++ b/src/colorizer/ColorizeCanvas.ts @@ -22,7 +22,6 @@ import { } from "three"; import { MAX_FEATURE_CATEGORIES } from "../constants"; -import { IControllableCanvas } from "./canvas"; import { DrawMode, FeatureDataType, OUT_OF_RANGE_COLOR_DEFAULT, OUTLIER_COLOR_DEFAULT } from "./types"; import { packDataTexture } from "./utils/texture_utils"; @@ -109,7 +108,7 @@ const getDefaultUniforms = (): ColorizeUniforms => { }; }; -export default class ColorizeCanvas implements IControllableCanvas { +export default class ColorizeCanvas { private geometry: PlaneGeometry; private material: ShaderMaterial; private pickMaterial: ShaderMaterial; diff --git a/src/colorizer/TimeControls.ts b/src/colorizer/TimeControls.ts index e82cc3af6..218a64e91 100644 --- a/src/colorizer/TimeControls.ts +++ b/src/colorizer/TimeControls.ts @@ -1,5 +1,6 @@ import { DEFAULT_PLAYBACK_FPS } from "../constants"; -import { IControllableCanvas } from "./canvas"; + +import CanvasWithOverlay from "./CanvasWithOverlay"; // time / playback controls const NO_TIMER_ID = -1; @@ -9,11 +10,11 @@ export default class TimeControls { private setFrameFn?: (frame: number) => Promise; private playbackFps: number; - private canvas: IControllableCanvas; + private canvas: CanvasWithOverlay; private pauseCallbacks: (() => void)[]; - constructor(canvas: IControllableCanvas, playbackFps: number = DEFAULT_PLAYBACK_FPS) { + constructor(canvas: CanvasWithOverlay, playbackFps: number = DEFAULT_PLAYBACK_FPS) { this.canvas = canvas; this.timerId = NO_TIMER_ID; this.pauseCallbacks = []; @@ -90,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/canvas/IControllableCanvas.ts b/src/colorizer/canvas/IControllableCanvas.ts deleted file mode 100644 index 43849ee5a..000000000 --- a/src/colorizer/canvas/IControllableCanvas.ts +++ /dev/null @@ -1,14 +0,0 @@ -import Dataset from "../Dataset"; - -export interface IControllableCanvas { - getTotalFrames(): number; - getCurrentFrame(): number; - setFrame(frame: number): Promise; - - render(): void; - - setDataset(dataset: Dataset): void; - setFeatureKey(key: string): void; - - domElement: HTMLCanvasElement; -} diff --git a/src/colorizer/canvas/index.ts b/src/colorizer/canvas/index.ts deleted file mode 100644 index e44673a38..000000000 --- a/src/colorizer/canvas/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IControllableCanvas } from "./IControllableCanvas"; - -export type { IControllableCanvas }; From 661d91bea78a067e2b57c099ef44db2cf67b97f6 Mon Sep 17 00:00:00 2001 From: Peyton Lee Date: Thu, 26 Sep 2024 10:42:14 -0700 Subject: [PATCH 6/6] doc: Updated comment, linting --- src/colorizer/CanvasWithOverlay.ts | 3 ++- src/colorizer/index.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/colorizer/CanvasWithOverlay.ts b/src/colorizer/CanvasWithOverlay.ts index 3d0add2fc..131df93ab 100644 --- a/src/colorizer/CanvasWithOverlay.ts +++ b/src/colorizer/CanvasWithOverlay.ts @@ -400,7 +400,8 @@ export default class CanvasWithOverlay extends ColorizeCanvas { //Clear canvas this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - // Render the viewport + // 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); diff --git a/src/colorizer/index.ts b/src/colorizer/index.ts index 4d1a52a44..6e396b842 100644 --- a/src/colorizer/index.ts +++ b/src/colorizer/index.ts @@ -5,7 +5,6 @@ import UrlArrayLoader from "./loaders/UrlArrayLoader"; import Plotting from "./Plotting"; import Track from "./Track"; -export type { IControllableCanvas } from "./canvas/IControllableCanvas"; export type { ArraySource, IArrayLoader, ITextureImageLoader } from "./loaders/ILoader"; export { ColorRamp, ColorRampType, Dataset, ImageFrameLoader, UrlArrayLoader as JsonArrayLoader, Plotting, Track };