Skip to content

Commit

Permalink
Merge pull request #93 from synesthesia-project/functional-components
Browse files Browse the repository at this point in the history
Rewrite LayersAndTimeline as a functional component
  • Loading branch information
s0 authored Mar 21, 2023
2 parents 370c6d2 + 0c68fdf commit 852a632
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 134 deletions.
269 changes: 135 additions & 134 deletions composer/src/scripts/ts/components/layers-and-timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import * as file from '@synesthesia-project/core/lib/file';
import * as selection from '../data/selection';
import * as util from '@synesthesia-project/core/lib/util';
import * as stageState from '../data/stage-state';
import * as playState from '../data/play-state';
import { PlayState, PlayStateData } from '../data/play-state';
import { useDebouncedState } from './util/debounce';

export interface LayersAndTimelineProps {
// Properties
className?: string;
selection: selection.Selection;
file: file.CueFile | null;
state: stageState.StageState;
playState: playState.PlayState;
playState: PlayState;
bindingLayer: number | null;
midiLayerBindings: { input: string; note: number; layer: number }[];
// Callbacks
Expand All @@ -27,158 +28,158 @@ export interface LayersAndTimelineProps {
toggleZoomPanLock: () => void;
}

export interface LayersAndTimelineState {
/** Current position in milliseconds, updated every so often based on frame-rate */
positionMillis: number;
mousePosition: number | null;
let last = 0;

const LayersAndTimeline: React.FunctionComponent<LayersAndTimelineProps> = ({
className,
selection,
file,
state,
playState,
bindingLayer,
midiLayerBindings,
timelineRef,
layersRef,
updateCueFile,
updateSelection,
requestBindingForLayer,
openLayerOptions,
toggleZoomPanLock,
}) => {
const now = performance.now();
console.log('render', Math.floor(now - last));
last = now;
const nextUpdate = React.useRef<{
animationFrame: number;
playState: PlayState;
}>({
animationFrame: -1,
playState,
});
/**
* Current position in milliseconds, updated every so often based on frame-rate
*/
const [positionMillis, setPositionMillis] = React.useState<number>(0);
// TODO: set up some kind of debouncer for this to take it at framerate
const [mousePosition, setMousePosition] = useDebouncedState<number | null>(
null
);
/**
* If the user is currently dragging the selected elements, then
* this is the difference in milliseconds
*/
selectionDraggingDiff: number | null;
}

class LayersAndTimeline extends React.Component<
LayersAndTimelineProps,
LayersAndTimelineState
> {
private updateInterval = -1;

constructor(props: LayersAndTimelineProps) {
super(props);
this.state = {
positionMillis: 0,
mousePosition: null,
selectionDraggingDiff: null,
};
}

public componentDidMount() {
this.updatePositionInterval(this.props);
}
const [selectionDraggingDiff, setSelectionDraggingDiff] = useDebouncedState<
number | null
>(null);

public componentWillReceiveProps(newProps: LayersAndTimelineProps) {
this.updatePositionInterval(newProps);
}

private updatePositionInterval = (newProps: LayersAndTimelineProps) => {
cancelAnimationFrame(this.updateInterval);
const playState = newProps.playState;
if (playState) {
// Start a re-rendering interval if currently playing
if (playState.state.type === 'playing') {
const update = () => {
this.updatePosition(playState);
this.updateInterval = requestAnimationFrame(update);
};
this.updateInterval = requestAnimationFrame(update);
}
this.updatePosition(playState);
}
};

private updatePosition = (playState: playState.PlayStateData) => {
const updatePosition = (playState: PlayStateData) => {
const time =
playState.state.type === 'paused'
? playState.state.positionMillis
: (performance.now() - playState.state.effectiveStartTimeMillis) *
playState.state.playSpeed;
// Update positionMillis with time if different enough
if (
time < this.state.positionMillis - 10 ||
time > this.state.positionMillis + 10
)
this.setState({ positionMillis: time });
setPositionMillis((current) =>
time < current - 10 || time > current + 10 ? time : current
);
};

private updateMouseHover = (mousePosition: number | null) => {
this.setState({ mousePosition });
};
React.useEffect(() => {
const update = () => {
if (nextUpdate.current.playState) {
updatePosition(nextUpdate.current.playState);
}
nextUpdate.current.animationFrame = requestAnimationFrame(update);
};
nextUpdate.current.animationFrame = requestAnimationFrame(update);
return () => {
cancelAnimationFrame(nextUpdate.current.animationFrame);
};
}, []);

private updateSelectionDraggingDiff = (
selectionDraggingDiff: number | null
) => {
this.setState({ selectionDraggingDiff });
};
React.useEffect(() => {
nextUpdate.current.playState = playState;
if (playState) {
updatePosition(playState);
}
}, [playState]);

public render() {
let layers: JSX.Element[] | null = null;
if (this.props.file) {
const file = this.props.file;
layers = this.props.file.layers.map((layer, i) => (
const layers = file
? file.layers.map((layer, i) => (
<Layer
key={i}
file={file}
layerKey={i}
layer={layer}
zoom={this.props.state.zoomPan}
selection={this.props.selection}
positionMillis={this.state.positionMillis}
bindingLayer={this.props.bindingLayer}
midiLayerBindings={this.props.midiLayerBindings}
selectionDraggingDiff={this.state.selectionDraggingDiff}
updateSelection={this.props.updateSelection}
updateCueFile={this.props.updateCueFile}
requestBindingForLayer={this.props.requestBindingForLayer}
updateSelectionDraggingDiff={this.updateSelectionDraggingDiff}
openLayerOptions={this.props.openLayerOptions}
{...{
key: i,
file,
layer,
layerKey: i,
zoom: state.zoomPan,
selection,
positionMillis,
bindingLayer,
midiLayerBindings,
selectionDraggingDiff,
updateSelection,
updateCueFile,
requestBindingForLayer,
updateSelectionDraggingDiff: setSelectionDraggingDiff,
openLayerOptions,
}}
/>
));
}

const playerPosition = this.props.file
? this.state.positionMillis / this.props.file.lengthMillis
: null;

const zoomMargin = stageState.relativeZoomMargins(
this.props.state.zoomPan,
playerPosition || 0
);

return (
<div className={this.props.className}>
<div className="layers" ref={(layers) => this.props.layersRef(layers)}>
{layers}
<div className="overlay">
<div
className="zoom"
style={{
left: -zoomMargin.left * 100 + '%',
right: -zoomMargin.right * 100 + '%',
}}
>
{playerPosition !== null && (
<div
className="marker player-position"
style={{ left: playerPosition * 100 + '%' }}
/>
)}
{this.state.mousePosition !== null && (
<div
className="marker mouse"
style={{ left: this.state.mousePosition * 100 + '%' }}
/>
)}
</div>
))
: null;

const playerPosition = file ? positionMillis / file.lengthMillis : null;

const zoomMargin = stageState.relativeZoomMargins(
state.zoomPan,
playerPosition || 0
);

return (
<div className={className}>
<div className="layers" ref={layersRef}>
{layers}
<div className="overlay">
<div
className="zoom"
style={{
left: -zoomMargin.left * 100 + '%',
right: -zoomMargin.right * 100 + '%',
}}
>
{playerPosition !== null && (
<div
className="marker player-position"
style={{ left: playerPosition * 100 + '%' }}
/>
)}
{mousePosition !== null && (
<div
className="marker mouse"
style={{ left: mousePosition * 100 + '%' }}
/>
)}
</div>
</div>
{this.props.file && (
<Timeline
timelineRef={this.props.timelineRef}
updateCueFile={this.props.updateCueFile}
file={this.props.file}
zoom={this.props.state.zoomPan}
positionMillis={this.state.positionMillis}
playState={this.props.playState}
updateMouseHover={this.updateMouseHover}
mousePosition={this.state.mousePosition}
toggleZoomPanLock={this.props.toggleZoomPanLock}
/>
)}
</div>
);
}
}
{file && (
<Timeline
{...{
timelineRef,
updateCueFile,
file,
zoom: state.zoomPan,
positionMillis,
playState,
updateMouseHover: setMousePosition,
mousePosition,
toggleZoomPanLock,
}}
/>
)}
</div>
);
};

const StyledLayersAndTimeline = styled(LayersAndTimeline)`
flex-grow: 1;
Expand Down
14 changes: 14 additions & 0 deletions composer/src/scripts/ts/components/util/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useState, useRef } from 'react';

export const useDebouncedState = <T>(init: T): [T, (newValue: T) => void] => {
const [state, setState] = useState(init);
const frame = useRef<number>(-1);

return [
state,
(newValue) => {
cancelAnimationFrame(frame.current);
frame.current = requestAnimationFrame(() => setState(newValue));
},
];
};

0 comments on commit 852a632

Please sign in to comment.