Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 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
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
3 changes: 2 additions & 1 deletion FlipViewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "npx webpack --mode production"
"build": "npx webpack --mode production",
"build-dev": "npx webpack --mode development"
},
"author": "Pascal Grittmann",
"license": "MIT",
Expand Down
170 changes: 164 additions & 6 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 } 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);
}

// to get only key presses and not is down state
// Idea is to only fire events if state of key changes
const keyPressed = new Set<string>();

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

Expand All @@ -25,26 +52,57 @@ 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 +115,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 @@ -75,6 +137,7 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {

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

this.state = {
selectedIdx: 0,
hideTools: props.hideTools
Expand All @@ -85,10 +148,35 @@ 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 && keyPressed.has(evt.key))
{
keyPressed.delete(evt.key);
evt.preventDefault();

if(keyPressed.size == 0)
setKeyPressed(false);

this.props.onKeyImageContainer(this.state.selectedIdx, this.props.idStr, evt.key, false);
}
}

onKeyDown(evt: React.KeyboardEvent<HTMLDivElement>) {
// trigger callbacks
if(this.props.onKeyImageContainer && !keyPressed.has(evt.key))
{
keyPressed.add(evt.key);
evt.preventDefault();
setKeyPressed(true);
this.props.onKeyImageContainer(this.state.selectedIdx, this.props.idStr, evt.key, true);
}

let newIdx = this.state.selectedIdx;
if (evt.key === "ArrowLeft" || evt.key === "ArrowDown") {
newIdx = this.state.selectedIdx - 1;
Expand Down Expand Up @@ -138,7 +226,13 @@ 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();
}
Expand All @@ -147,6 +241,9 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
this.setState({hideTools: !this.state.hideTools});
evt.stopPropagation();
}


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

reset() {
Expand Down Expand Up @@ -236,6 +333,13 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
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 {
let popup = null;
if (this.state.popupContent) {
Expand All @@ -249,7 +353,7 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
}

return (
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown}>
<div className={styles['flipbook']} style={this.props.style} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp}>
<div style={{display: "contents"}}>
<MethodList
names={this.props.names}
Expand All @@ -265,6 +369,9 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
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}
Expand Down Expand Up @@ -302,16 +409,51 @@ export class FlipBook extends React.Component<FlipProps, FlipState> {
}
}


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);
}
}

componentDidMount(): void {
if (this.props.initialZoom)
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 Down Expand Up @@ -381,8 +523,14 @@ export type FlipBookParams = {
initialTMO: ToneMapSettings,
initialTMOOverrides: ToneMapSettings[],
onClick?: OnClickHandler,
onWheel?: OnWheelHandler,
onMouseOver?: OnMouseOverHandler,
onKeyImageContainer?: OnKeyHandler,
// onKeyUpIC?: OnKeyUpHandler,
colorTheme?: string,
hideTools: boolean,
containerId: string,
id: string,
}

export function AddFlipBook(params: FlipBookParams, groupName?: string) {
Expand Down Expand Up @@ -415,8 +563,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,14 +577,22 @@ 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});
Expand Down
Loading