Skip to content

Commit a20f159

Browse files
OpaKnoppipgrit
andauthored
FlipBook: Image replacement and event handlers (#15)
* template changes * Flipbook key is now a string instead of a int[], getImage() and UpdateImage() only really take Images instead of whole Tuple * changed key from int[] to string in flipviewer ts files * combine new UpdateImage() with Generate() method * set zoom on Alt, set crop on shift, any keyPress disables default html scrolling so that user can add various number of custom actions keys on c# side * reset update interval, fixed image update w/o awaits * added comments, clean up * removed old comments, set zoom to be ctrl+wheel * fixed magifier disappearing, but not sure for jupyter * tweak script for dev build * fixed magnifier while zooming * move abs error computation to where it belongs * clean up * Revert "fixed magnifier while zooming" This reverts commit 662d44d. * Revert "fixed magifier disappearing, but not sure for jupyter" This reverts commit 7aa32e5. * cleaning * changed Magnifier to find best position within Flipbook borders for itself * renamed Key to ID for the dictionaries * changed resolution of magnifier, changed decision making of where to place magnifier around mouse (prettier) * changed onKeyIC to onKeyImageContainer * changed comment to trigger callbacks. Other languages also can generate Flipbooks * change comment. Not only c# will be supported * removed code duplication * simplified code * changed listeners to send complete listener state (and keep track of state also on the js side) * clean up Flipbook.tsx * clean up Flipviewer.ts * clean up * removed typo, added comment for XY Coords of Mouse in StateListener * moved magnifier vars in ImageContainerProps * forgot to add to last commit * added default position for magnifier. If clicked again and default position is possible -> set to default position * changed events to send hashset of pressed keys * changed listener parameters * fix build on first clone * formatting * removed old comments * formatting * update dependencies --------- Co-authored-by: Pascal Grittmann <grittmann@cg.uni-saarland.de>
1 parent b468023 commit a20f159

11 files changed

Lines changed: 584 additions & 113 deletions

File tree

FlipViewer/src/FlipBook.tsx

Lines changed: 180 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createRoot } from 'react-dom/client';
22
import styles from './styles.module.css';
33
import React, { createRef } from 'react';
44
import { renderImage } from "./Render";
5-
import { ImageContainer, OnClickHandler } from './ImageContainer';
5+
import { ImageContainer, OnClickHandler, OnWheelHandler, OnMouseOverHandler, ImageContainerState, OnKeyHandler, setKeyPressed, listenerState } from './ImageContainer';
66
import { ToneMapControls } from './ToneMapControls';
77
import { MethodList } from './MethodList';
88
import { Tools } from './Tools';
@@ -11,9 +11,36 @@ import { ToneMapSettings, ZoomLevel } from './flipviewer';
1111

1212
const UPDATE_INTERVAL_MS = 100;
1313

14+
// Registry to update flipbooks
15+
export type BookRef = React.RefObject<FlipBook>;
16+
const registry = new Map<string, Set<BookRef>>();
17+
18+
export function getBooks(id: string): BookRef[] {
19+
return Array.from(registry.get(id) ?? new Set());
20+
}
21+
22+
export function registerBook(id: string, ref: BookRef) {
23+
if (!id) return;
24+
const set = registry.get(id) ?? new Set<BookRef>();
25+
set.add(ref);
26+
registry.set(id, set);
27+
}
28+
29+
export function unregisterBook(id: string, ref: BookRef) {
30+
const set = registry.get(id);
31+
if (!set) return;
32+
set.delete(ref);
33+
if (set.size === 0) registry.delete(id);
34+
}
35+
36+
// Keep track of pressed keys
37+
// Idea is to only fire events if state of key changes
38+
const keysPressed = new Set<string>();
39+
1440
export class ToneMappingImage {
1541
currentTMO: string;
1642
dirty: boolean;
43+
isPixelUpdate: boolean;
1744
canvas: HTMLCanvasElement;
1845
pixels: Float32Array | ImageData;
1946

@@ -25,26 +52,52 @@ export class ToneMappingImage {
2552

2653
let hdrImg = this;
2754
setInterval(function() {
28-
if (!hdrImg.dirty) return;
29-
hdrImg.dirty = false;
55+
if (!hdrImg.dirty || hdrImg.isPixelUpdate)
56+
return;
3057
renderImage(hdrImg.canvas, hdrImg.pixels, hdrImg.currentTMO);
58+
hdrImg.dirty = false;
3159
onAfterRender();
3260
}, UPDATE_INTERVAL_MS)
3361
}
3462
apply(tmo: string) {
3563
this.currentTMO = tmo;
3664
this.dirty = true;
3765
}
66+
setPixels(p: Float32Array | ImageData) {
67+
this.isPixelUpdate = true;
68+
this.pixels = p;
69+
this.dirty = false;
70+
renderImage(this.canvas, this.pixels, this.currentTMO);
71+
this.isPixelUpdate = false;
72+
}
3873
}
3974

75+
4076
type SelectUpdateFn = (groupName: string, newIdx: number) => void;
4177
var selectUpdateListeners: SelectUpdateFn[] = [];
4278

79+
80+
type TMOUpdateFn = (groupName: string, newTMOSettings: ToneMapSettings) => void;
81+
var tmoUpdateListeners: TMOUpdateFn[] = [];
82+
83+
type imageConStateUpdateFn = (groupName: string, newImgConState: ImageContainerState) => void;
84+
var imgConStateUpdateListeners: imageConStateUpdateFn[] = [];
85+
4386
export function SetGroupIndex(groupName: string, newIdx: number) {
4487
for (let fn of selectUpdateListeners)
4588
fn(groupName, newIdx);
4689
}
4790

91+
export function SetGroupTMOSettings(groupName: string, newTMOSettings: ToneMapSettings) {
92+
for (let fn of tmoUpdateListeners)
93+
fn(groupName, newTMOSettings);
94+
}
95+
96+
export function SetGroupImageContainerSettings(groupName: string, newImgConState: ImageContainerState) {
97+
for (let fn of imgConStateUpdateListeners)
98+
fn(groupName, newImgConState);
99+
}
100+
48101
export interface FlipProps {
49102
names: string[];
50103
width: number;
@@ -57,11 +110,15 @@ export interface FlipProps {
57110
initialTMOOverrides: ToneMapSettings[];
58111
style?: React.CSSProperties;
59112
onClick?: OnClickHandler;
113+
onWheel?: OnWheelHandler;
114+
onMouseOver?: OnMouseOverHandler;
115+
onKeyImageContainer?: OnKeyHandler;
60116
groupName?: string;
61117
hideTools: boolean;
118+
idStr: string;
62119
}
63120

64-
interface FlipState {
121+
export interface FlipState {
65122
selectedIdx: number;
66123
popupContent?: React.ReactNode;
67124
popupDurationMs?: number;
@@ -73,8 +130,9 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
73130
imageContainer: React.RefObject<ImageContainer>;
74131
tools: React.RefObject<Tools>;
75132

76-
constructor(props : FlipProps) {
133+
constructor(props: FlipProps) {
77134
super(props);
135+
78136
this.state = {
79137
selectedIdx: 0,
80138
hideTools: props.hideTools
@@ -85,10 +143,51 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
85143
this.tools = createRef();
86144

87145
this.onKeyDown = this.onKeyDown.bind(this);
146+
this.onKeyUp = this.onKeyUp.bind(this);
88147
this.onSelectUpdate = this.onSelectUpdate.bind(this);
148+
this.onTMOUpdate = this.onTMOUpdate.bind(this);
149+
}
150+
151+
onKeyUp(evt: React.KeyboardEvent<HTMLDivElement>) {
152+
// trigger callbacks
153+
if (this.props.onKeyImageContainer && keysPressed.has(evt.key)) {
154+
keysPressed.delete(evt.key);
155+
evt.preventDefault();
156+
157+
if (keysPressed.size == 0)
158+
this.imageContainer.current.setState({
159+
isAnyKeyPressed: false,
160+
}, () => {
161+
this.imageContainer.current.props.onStateChange?.(this.imageContainer.current.state); // callback
162+
});
163+
164+
listenerState.selectedIdx = this.state.selectedIdx;
165+
listenerState.ID = this.props.idStr;
166+
listenerState.keysPressed = keysPressed;
167+
168+
this.props.onKeyImageContainer(listenerState.mouseX, listenerState.mouseY, listenerState.ID, listenerState.selectedIdx, Array.from(listenerState.keysPressed));
169+
}
89170
}
90171

91172
onKeyDown(evt: React.KeyboardEvent<HTMLDivElement>) {
173+
// trigger callbacks
174+
if (this.props.onKeyImageContainer && !keysPressed.has(evt.key)) {
175+
keysPressed.add(evt.key);
176+
evt.preventDefault();
177+
178+
this.imageContainer.current.setState({
179+
isAnyKeyPressed: true,
180+
}, () => {
181+
this.imageContainer.current.props.onStateChange?.(this.imageContainer.current.state); // callback
182+
});
183+
184+
listenerState.selectedIdx = this.state.selectedIdx;
185+
listenerState.ID = this.props.idStr;
186+
listenerState.keysPressed = keysPressed;
187+
188+
this.props.onKeyImageContainer(listenerState.mouseX, listenerState.mouseY, listenerState.ID, listenerState.selectedIdx, Array.from(listenerState.keysPressed));
189+
}
190+
92191
let newIdx = this.state.selectedIdx;
93192
if (evt.key === "ArrowLeft" || evt.key === "ArrowDown") {
94193
newIdx = this.state.selectedIdx - 1;
@@ -138,15 +237,23 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
138237
evt.stopPropagation();
139238
}
140239

141-
if (evt.key === "r") {
240+
if (evt.ctrlKey && evt.key === 'r') {
241+
this.tmoCtrls.current.state.globalSettings.exposure = 0;
242+
evt.stopPropagation();
243+
evt.preventDefault();
244+
}
245+
246+
if (!evt.ctrlKey && evt.key === "r") {
142247
this.reset();
143248
evt.stopPropagation();
144249
}
145250

146251
if (evt.key === "t") {
147-
this.setState({hideTools: !this.state.hideTools});
252+
this.setState({ hideTools: !this.state.hideTools });
148253
evt.stopPropagation();
149254
}
255+
256+
this.updateTMOSettings(this.tmoCtrls.current.state.globalSettings);
150257
}
151258

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

234341
updateSelection(newIdx: number) {
235342
if (this.props.groupName) SetGroupIndex(this.props.groupName, newIdx);
236-
else this.setState({selectedIdx: newIdx});
343+
else this.setState({ selectedIdx: newIdx });
344+
}
345+
346+
updateTMOSettings(newTMOSettings: ToneMapSettings) {
347+
if (this.props.groupName) SetGroupTMOSettings(this.props.groupName, newTMOSettings);
348+
else this.tmoCtrls.current.applySettings(newTMOSettings);
349+
237350
}
238351

239352
render(): React.ReactNode {
@@ -242,36 +355,39 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
242355
popup =
243356
<Popup
244357
durationMs={this.state.popupDurationMs}
245-
unmount={() => this.setState({popupContent: null})}
358+
unmount={() => this.setState({ popupContent: null })}
246359
>
247360
{this.state.popupContent}
248361
</Popup>
249362
}
250363

251364
return (
252-
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown}>
253-
<div style={{display: "contents"}}>
365+
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp}>
366+
<div style={{ display: "contents" }}>
254367
<MethodList
255368
names={this.props.names}
256369
selectedIdx={this.state.selectedIdx}
257370
setSelectedIdx={this.updateSelection.bind(this)}
258371
/>
259372
<ImageContainer ref={this.imageContainer}
260-
width = {this.props.width}
261-
height = {this.props.height}
373+
width={this.props.width}
374+
height={this.props.height}
262375
rawPixels={this.props.rawPixels}
263376
means={this.props.means}
264377
toneMappers={this.props.toneMappers}
265378
selectedIdx={this.state.selectedIdx}
266379
onZoom={(zoom) => this.tools.current.onZoom(zoom)}
267380
onClick={this.props.onClick}
381+
onWheel={this.props.onWheel}
382+
onMouseOver={this.props.onMouseOver}
383+
onStateChange={(st) => this.onImageContainerUpdate(st)}
268384
>
269385
{popup}
270386
<button className={styles.toolsBtn}
271-
onClick={() => this.setState({hideTools: !this.state.hideTools})}
272-
style={{position: "absolute", bottom: 0, right: 0}}
387+
onClick={() => this.setState({ hideTools: !this.state.hideTools })}
388+
style={{ position: "absolute", bottom: 0, right: 0 }}
273389
>
274-
{ this.state.hideTools ? "Show tools " : "Hide tools " }
390+
{this.state.hideTools ? "Show tools " : "Hide tools "}
275391
<span className={styles['key']}>t</span>
276392
</button>
277393
</ImageContainer>
@@ -298,7 +414,29 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
298414
onSelectUpdate(groupName: string, newIdx: number) {
299415
if (groupName == this.props.groupName) {
300416
newIdx = Math.min(this.props.rawPixels.length - 1, Math.max(0, newIdx));
301-
this.setState({selectedIdx: newIdx});
417+
this.setState({ selectedIdx: newIdx });
418+
}
419+
}
420+
421+
onTMOUpdate(groupName: string, newTMOSettings: ToneMapSettings) {
422+
if (groupName == this.props.groupName) {
423+
this.tmoCtrls.current.applySettings(newTMOSettings);
424+
}
425+
}
426+
427+
// is called when onStateIsChanged in ImageContainer is called
428+
// everytime when the ImageContainerState changes (pos, zoom, etc.)
429+
// calls onImageContainerGroupUpdate = ()
430+
onImageContainerUpdate(newImageContainerState: ImageContainerState) {
431+
if (this.props.groupName) {
432+
SetGroupImageContainerSettings(this.props.groupName, newImageContainerState);
433+
}
434+
}
435+
436+
// is called when other flipbook's ImageContainerStates changes
437+
onImageContainerGroupUpdate = (groupName: string, newImageContainerState: ImageContainerState) => {
438+
if (groupName === this.props.groupName && this.imageContainer.current) {
439+
this.imageContainer.current.setState(newImageContainerState);
302440
}
303441
}
304442

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

309447
selectUpdateListeners.push(this.onSelectUpdate);
448+
tmoUpdateListeners.push(this.onTMOUpdate);
449+
imgConStateUpdateListeners.push(this.onImageContainerGroupUpdate);
310450
}
311451

312452
componentWillUnmount(): void {
313453
let idx = selectUpdateListeners.findIndex(v => v === this.onSelectUpdate);
314454
selectUpdateListeners.splice(idx, 1);
455+
456+
idx = tmoUpdateListeners.findIndex(v => v === this.onTMOUpdate);
457+
tmoUpdateListeners.splice(idx, 1);
458+
459+
idx = imgConStateUpdateListeners.findIndex(v => v === this.onImageContainerGroupUpdate);
460+
imgConStateUpdateListeners.splice(idx, 1);
315461
}
316462

317463
connect(other: React.RefObject<FlipBook>) {
@@ -324,7 +470,7 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
324470
* @param images List of images that are either raw floating point data or HTML image elements
325471
* @returns List of all images with image elements replaced by their image data
326472
*/
327-
function GetImageData(images: (Float32Array | HTMLImageElement)[]) : (Float32Array | ImageData)[] {
473+
function GetImageData(images: (Float32Array | HTMLImageElement)[]): (Float32Array | ImageData)[] {
328474
let imgData: (Float32Array | ImageData)[] = []
329475
for (let i = 0; i < images.length; ++i) {
330476
if (images[i] instanceof HTMLImageElement) {
@@ -381,8 +527,13 @@ export type FlipBookParams = {
381527
initialTMO: ToneMapSettings,
382528
initialTMOOverrides: ToneMapSettings[],
383529
onClick?: OnClickHandler,
530+
onWheel?: OnWheelHandler,
531+
onMouseOver?: OnMouseOverHandler,
532+
onKeyImageContainer?: OnKeyHandler,
384533
colorTheme?: string,
385534
hideTools: boolean,
535+
containerId: string,
536+
id: string,
386537
}
387538

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

417568
const root = createRoot(params.parentElement);
569+
const bookRef = createRef<FlipBook>();
418570
root.render(
419571
<FlipBook
572+
ref={bookRef}
420573
names={params.names}
421574
width={params.width}
422575
height={params.height}
@@ -427,15 +580,23 @@ export function AddFlipBook(params: FlipBookParams, groupName?: string) {
427580
initialTMO={params.initialTMO}
428581
initialTMOOverrides={params.initialTMOOverrides}
429582
onClick={params.onClick}
583+
onWheel={params.onWheel}
584+
onMouseOver={params.onMouseOver}
585+
onKeyImageContainer={params.onKeyImageContainer}
430586
style={themeStyle}
431587
groupName={groupName}
432588
hideTools={params.hideTools}
589+
idStr={params.id}
433590
/>
434591
);
435592

593+
if (params.id)
594+
registerBook(params.id, bookRef);
595+
436596
new MutationObserver(_ => {
437597
if (!document.body.contains(params.parentElement)) {
598+
unregisterBook(params.id, bookRef);
438599
root.unmount();
439600
}
440-
}).observe(document.body, {childList: true, subtree: true});
601+
}).observe(document.body, { childList: true, subtree: true });
441602
}

0 commit comments

Comments
 (0)