Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
28aa481
template changes
OpaKnoppi Oct 6, 2025
47f2992
Flipbook key is now a string instead of a int[], getImage() and Updat…
OpaKnoppi Nov 23, 2025
0a4bc3c
changed key from int[] to string in flipviewer ts files
OpaKnoppi Nov 23, 2025
d281ab9
combine new UpdateImage() with Generate() method
OpaKnoppi Nov 24, 2025
7eb0b83
set zoom on Alt, set crop on shift, any keyPress disables default htm…
OpaKnoppi Nov 24, 2025
a92eab2
reset update interval, fixed image update w/o awaits
OpaKnoppi Nov 26, 2025
7927409
added comments, clean up
OpaKnoppi Nov 26, 2025
cc9b9fd
removed old comments, set zoom to be ctrl+wheel
OpaKnoppi Nov 26, 2025
7aa32e5
fixed magifier disappearing, but not sure for jupyter
OpaKnoppi Dec 21, 2025
393c52f
tweak script for dev build
pgrit Dec 23, 2025
662d44d
fixed magnifier while zooming
OpaKnoppi Jan 5, 2026
c7771ba
merge error - hope everthing is fine...
OpaKnoppi Jan 5, 2026
a62e699
move abs error computation to where it belongs
pgrit Jan 15, 2026
3b4fd9a
clean up
pgrit Jan 15, 2026
d88ba8e
Revert "fixed magnifier while zooming"
pgrit Jan 15, 2026
984aac2
Revert "fixed magifier disappearing, but not sure for jupyter"
pgrit Jan 15, 2026
6c27dd1
cleaning
pgrit Jan 15, 2026
ead8988
changed Magnifier to find best position within Flipbook borders for i…
OpaKnoppi Jan 19, 2026
162f0e2
renamed Key to ID for the dictionaries
OpaKnoppi Jan 19, 2026
e2d4d49
changed resolution of magnifier, changed decision making of where to …
OpaKnoppi Jan 22, 2026
4060249
changed onKeyIC to onKeyImageContainer
OpaKnoppi Jan 22, 2026
b86c0b2
changed comment to trigger callbacks. Other languages also can genera…
OpaKnoppi Jan 22, 2026
8e75dd4
change comment. Not only c# will be supported
OpaKnoppi Jan 22, 2026
60970f2
removed code duplication
OpaKnoppi Jan 22, 2026
ea81e0b
simplified code
OpaKnoppi Jan 22, 2026
b10bb6c
changed listeners to send complete listener state (and keep track of …
OpaKnoppi Jan 31, 2026
bb7b419
clean up Flipbook.tsx
OpaKnoppi Jan 31, 2026
6e59f30
clean up Flipviewer.ts
OpaKnoppi Jan 31, 2026
c442afb
clean up
OpaKnoppi Jan 31, 2026
d4484e5
removed typo, added comment for XY Coords of Mouse in StateListener
OpaKnoppi Feb 2, 2026
663be16
moved magnifier vars in ImageContainerProps
OpaKnoppi Feb 12, 2026
1a5e6a5
forgot to add to last commit
OpaKnoppi Feb 12, 2026
1b71ad5
added default position for magnifier. If clicked again and default po…
OpaKnoppi Feb 12, 2026
8c83c01
changed events to send hashset of pressed keys
OpaKnoppi Feb 12, 2026
286f340
changed listener parameters
OpaKnoppi Feb 16, 2026
50d18d6
fix build on first clone
pgrit Feb 17, 2026
3e8a284
formatting
pgrit Feb 17, 2026
585cee6
removed old comments
OpaKnoppi Feb 18, 2026
2559ed3
merges?
OpaKnoppi Feb 18, 2026
db3f48a
formatting
pgrit Feb 18, 2026
9123504
Merge branch 'master' into OpaKnoppi/master
pgrit Feb 18, 2026
1b4eea8
update dependencies
pgrit Feb 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 180 additions & 19 deletions FlipViewer/src/FlipBook.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client';
import styles from './styles.module.css';
import React, { createRef } from 'react';
import { renderImage } from "./Render";
import { ImageContainer, OnClickHandler } from './ImageContainer';
import { ImageContainer, OnClickHandler, OnWheelHandler, OnMouseOverHandler, ImageContainerState, OnKeyHandler, setKeyPressed, listenerState } from './ImageContainer';
import { ToneMapControls } from './ToneMapControls';
import { MethodList } from './MethodList';
import { Tools } from './Tools';
Expand All @@ -11,9 +11,36 @@ import { ToneMapSettings, ZoomLevel } from './flipviewer';

const UPDATE_INTERVAL_MS = 100;

// Registry to update flipbooks
export type BookRef = React.RefObject<FlipBook>;
const registry = new Map<string, Set<BookRef>>();

export function getBooks(id: string): BookRef[] {
return Array.from(registry.get(id) ?? new Set());
}

export function registerBook(id: string, ref: BookRef) {
if (!id) return;
const set = registry.get(id) ?? new Set<BookRef>();
set.add(ref);
registry.set(id, set);
}

export function unregisterBook(id: string, ref: BookRef) {
const set = registry.get(id);
if (!set) return;
set.delete(ref);
if (set.size === 0) registry.delete(id);
}

// Keep track of pressed keys
// Idea is to only fire events if state of key changes
const keysPressed = new Set<string>();

export class ToneMappingImage {
currentTMO: string;
dirty: boolean;
isPixelUpdate: boolean;
canvas: HTMLCanvasElement;
pixels: Float32Array | ImageData;

Expand All @@ -25,26 +52,52 @@ export class ToneMappingImage {

let hdrImg = this;
setInterval(function() {
if (!hdrImg.dirty) return;
hdrImg.dirty = false;
if (!hdrImg.dirty || hdrImg.isPixelUpdate)
return;
renderImage(hdrImg.canvas, hdrImg.pixels, hdrImg.currentTMO);
hdrImg.dirty = false;
onAfterRender();
}, UPDATE_INTERVAL_MS)
}
apply(tmo: string) {
this.currentTMO = tmo;
this.dirty = true;
}
setPixels(p: Float32Array | ImageData) {
this.isPixelUpdate = true;
this.pixels = p;
this.dirty = false;
renderImage(this.canvas, this.pixels, this.currentTMO);
this.isPixelUpdate = false;
}
}


type SelectUpdateFn = (groupName: string, newIdx: number) => void;
var selectUpdateListeners: SelectUpdateFn[] = [];


type TMOUpdateFn = (groupName: string, newTMOSettings: ToneMapSettings) => void;
var tmoUpdateListeners: TMOUpdateFn[] = [];

type imageConStateUpdateFn = (groupName: string, newImgConState: ImageContainerState) => void;
var imgConStateUpdateListeners: imageConStateUpdateFn[] = [];

export function SetGroupIndex(groupName: string, newIdx: number) {
for (let fn of selectUpdateListeners)
fn(groupName, newIdx);
}

export function SetGroupTMOSettings(groupName: string, newTMOSettings: ToneMapSettings) {
for (let fn of tmoUpdateListeners)
fn(groupName, newTMOSettings);
}

export function SetGroupImageContainerSettings(groupName: string, newImgConState: ImageContainerState) {
for (let fn of imgConStateUpdateListeners)
fn(groupName, newImgConState);
}

export interface FlipProps {
names: string[];
width: number;
Expand All @@ -57,11 +110,15 @@ export interface FlipProps {
initialTMOOverrides: ToneMapSettings[];
style?: React.CSSProperties;
onClick?: OnClickHandler;
onWheel?: OnWheelHandler;
onMouseOver?: OnMouseOverHandler;
onKeyImageContainer?: OnKeyHandler;
groupName?: string;
hideTools: boolean;
idStr: string;
}

interface FlipState {
export interface FlipState {
selectedIdx: number;
popupContent?: React.ReactNode;
popupDurationMs?: number;
Expand All @@ -73,8 +130,9 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
imageContainer: React.RefObject<ImageContainer>;
tools: React.RefObject<Tools>;

constructor(props : FlipProps) {
constructor(props: FlipProps) {
super(props);

this.state = {
selectedIdx: 0,
hideTools: props.hideTools
Expand All @@ -85,10 +143,51 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
this.tools = createRef();

this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onSelectUpdate = this.onSelectUpdate.bind(this);
this.onTMOUpdate = this.onTMOUpdate.bind(this);
}

onKeyUp(evt: React.KeyboardEvent<HTMLDivElement>) {
// trigger callbacks
if (this.props.onKeyImageContainer && keysPressed.has(evt.key)) {
keysPressed.delete(evt.key);
evt.preventDefault();

if (keysPressed.size == 0)
this.imageContainer.current.setState({
isAnyKeyPressed: false,
}, () => {
this.imageContainer.current.props.onStateChange?.(this.imageContainer.current.state); // callback
});

listenerState.selectedIdx = this.state.selectedIdx;
listenerState.ID = this.props.idStr;
listenerState.keysPressed = keysPressed;

this.props.onKeyImageContainer(listenerState.mouseX, listenerState.mouseY, listenerState.ID, listenerState.selectedIdx, Array.from(listenerState.keysPressed));
}
}

onKeyDown(evt: React.KeyboardEvent<HTMLDivElement>) {
// trigger callbacks
if (this.props.onKeyImageContainer && !keysPressed.has(evt.key)) {
keysPressed.add(evt.key);
evt.preventDefault();

this.imageContainer.current.setState({
isAnyKeyPressed: true,
}, () => {
this.imageContainer.current.props.onStateChange?.(this.imageContainer.current.state); // callback
});

listenerState.selectedIdx = this.state.selectedIdx;
listenerState.ID = this.props.idStr;
listenerState.keysPressed = keysPressed;

this.props.onKeyImageContainer(listenerState.mouseX, listenerState.mouseY, listenerState.ID, listenerState.selectedIdx, Array.from(listenerState.keysPressed));
}

let newIdx = this.state.selectedIdx;
if (evt.key === "ArrowLeft" || evt.key === "ArrowDown") {
newIdx = this.state.selectedIdx - 1;
Expand Down Expand Up @@ -138,15 +237,23 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
evt.stopPropagation();
}

if (evt.key === "r") {
if (evt.ctrlKey && evt.key === 'r') {
this.tmoCtrls.current.state.globalSettings.exposure = 0;
evt.stopPropagation();
evt.preventDefault();
}

if (!evt.ctrlKey && evt.key === "r") {
this.reset();
evt.stopPropagation();
}

if (evt.key === "t") {
this.setState({hideTools: !this.state.hideTools});
this.setState({ hideTools: !this.state.hideTools });
evt.stopPropagation();
}

this.updateTMOSettings(this.tmoCtrls.current.state.globalSettings);
}

reset() {
Expand Down Expand Up @@ -233,7 +340,13 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {

updateSelection(newIdx: number) {
if (this.props.groupName) SetGroupIndex(this.props.groupName, newIdx);
else this.setState({selectedIdx: newIdx});
else this.setState({ selectedIdx: newIdx });
}

updateTMOSettings(newTMOSettings: ToneMapSettings) {
if (this.props.groupName) SetGroupTMOSettings(this.props.groupName, newTMOSettings);
else this.tmoCtrls.current.applySettings(newTMOSettings);

}

render(): React.ReactNode {
Expand All @@ -242,36 +355,39 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
popup =
<Popup
durationMs={this.state.popupDurationMs}
unmount={() => this.setState({popupContent: null})}
unmount={() => this.setState({ popupContent: null })}
>
{this.state.popupContent}
</Popup>
}

return (
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown}>
<div style={{display: "contents"}}>
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp}>
<div style={{ display: "contents" }}>
<MethodList
names={this.props.names}
selectedIdx={this.state.selectedIdx}
setSelectedIdx={this.updateSelection.bind(this)}
/>
<ImageContainer ref={this.imageContainer}
width = {this.props.width}
height = {this.props.height}
width={this.props.width}
height={this.props.height}
rawPixels={this.props.rawPixels}
means={this.props.means}
toneMappers={this.props.toneMappers}
selectedIdx={this.state.selectedIdx}
onZoom={(zoom) => this.tools.current.onZoom(zoom)}
onClick={this.props.onClick}
onWheel={this.props.onWheel}
onMouseOver={this.props.onMouseOver}
onStateChange={(st) => this.onImageContainerUpdate(st)}
>
{popup}
<button className={styles.toolsBtn}
onClick={() => this.setState({hideTools: !this.state.hideTools})}
style={{position: "absolute", bottom: 0, right: 0}}
onClick={() => this.setState({ hideTools: !this.state.hideTools })}
style={{ position: "absolute", bottom: 0, right: 0 }}
>
{ this.state.hideTools ? "Show tools " : "Hide tools " }
{this.state.hideTools ? "Show tools " : "Hide tools "}
<span className={styles['key']}>t</span>
</button>
</ImageContainer>
Expand All @@ -298,7 +414,29 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
onSelectUpdate(groupName: string, newIdx: number) {
if (groupName == this.props.groupName) {
newIdx = Math.min(this.props.rawPixels.length - 1, Math.max(0, newIdx));
this.setState({selectedIdx: newIdx});
this.setState({ selectedIdx: newIdx });
}
}

onTMOUpdate(groupName: string, newTMOSettings: ToneMapSettings) {
if (groupName == this.props.groupName) {
this.tmoCtrls.current.applySettings(newTMOSettings);
}
}

// is called when onStateIsChanged in ImageContainer is called
// everytime when the ImageContainerState changes (pos, zoom, etc.)
// calls onImageContainerGroupUpdate = ()
onImageContainerUpdate(newImageContainerState: ImageContainerState) {
if (this.props.groupName) {
SetGroupImageContainerSettings(this.props.groupName, newImageContainerState);
}
}

// is called when other flipbook's ImageContainerStates changes
onImageContainerGroupUpdate = (groupName: string, newImageContainerState: ImageContainerState) => {
if (groupName === this.props.groupName && this.imageContainer.current) {
this.imageContainer.current.setState(newImageContainerState);
}
}

Expand All @@ -307,11 +445,19 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
this.imageContainer.current.setZoom(this.props.initialZoom);

selectUpdateListeners.push(this.onSelectUpdate);
tmoUpdateListeners.push(this.onTMOUpdate);
imgConStateUpdateListeners.push(this.onImageContainerGroupUpdate);
}

componentWillUnmount(): void {
let idx = selectUpdateListeners.findIndex(v => v === this.onSelectUpdate);
selectUpdateListeners.splice(idx, 1);

idx = tmoUpdateListeners.findIndex(v => v === this.onTMOUpdate);
tmoUpdateListeners.splice(idx, 1);

idx = imgConStateUpdateListeners.findIndex(v => v === this.onImageContainerGroupUpdate);
imgConStateUpdateListeners.splice(idx, 1);
}

connect(other: React.RefObject<FlipBook>) {
Expand All @@ -324,7 +470,7 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
* @param images List of images that are either raw floating point data or HTML image elements
* @returns List of all images with image elements replaced by their image data
*/
function GetImageData(images: (Float32Array | HTMLImageElement)[]) : (Float32Array | ImageData)[] {
function GetImageData(images: (Float32Array | HTMLImageElement)[]): (Float32Array | ImageData)[] {
let imgData: (Float32Array | ImageData)[] = []
for (let i = 0; i < images.length; ++i) {
if (images[i] instanceof HTMLImageElement) {
Expand Down Expand Up @@ -381,8 +527,13 @@ export type FlipBookParams = {
initialTMO: ToneMapSettings,
initialTMOOverrides: ToneMapSettings[],
onClick?: OnClickHandler,
onWheel?: OnWheelHandler,
onMouseOver?: OnMouseOverHandler,
onKeyImageContainer?: OnKeyHandler,
colorTheme?: string,
hideTools: boolean,
containerId: string,
id: string,
}

export function AddFlipBook(params: FlipBookParams, groupName?: string) {
Expand Down Expand Up @@ -415,8 +566,10 @@ export function AddFlipBook(params: FlipBookParams, groupName?: string) {
let themeStyle = colorThemes[params.colorTheme ?? "dark"];

const root = createRoot(params.parentElement);
const bookRef = createRef<FlipBook>();
root.render(
<FlipBook
ref={bookRef}
names={params.names}
width={params.width}
height={params.height}
Expand All @@ -427,15 +580,23 @@ export function AddFlipBook(params: FlipBookParams, groupName?: string) {
initialTMO={params.initialTMO}
initialTMOOverrides={params.initialTMOOverrides}
onClick={params.onClick}
onWheel={params.onWheel}
onMouseOver={params.onMouseOver}
onKeyImageContainer={params.onKeyImageContainer}
style={themeStyle}
groupName={groupName}
hideTools={params.hideTools}
idStr={params.id}
/>
);

if (params.id)
registerBook(params.id, bookRef);

new MutationObserver(_ => {
if (!document.body.contains(params.parentElement)) {
unregisterBook(params.id, bookRef);
root.unmount();
}
}).observe(document.body, {childList: true, subtree: true});
}).observe(document.body, { childList: true, subtree: true });
}
Loading