feat: configurable framerate and resolution

This commit is contained in:
aarthificial
2022-06-03 23:39:24 +02:00
parent 895a53ab42
commit a591683f93
14 changed files with 236 additions and 39 deletions

View File

@@ -0,0 +1,17 @@
import styles from './Controls.module.scss';
import type {JSX} from 'preact';
import {classes} from '../../utils';
interface ButtonProps extends JSX.HTMLAttributes<HTMLButtonElement> {
main?: boolean;
}
export function Button(props: ButtonProps) {
return (
<button
className={classes(styles.button, [styles.main, props.main])}
type={'button'}
{...props}
/>
);
}

View File

@@ -22,17 +22,20 @@
height: 24px;
display: block;
cursor: pointer;
opacity: 0.54;
&.main {
opacity: 1;
&::after {
background-color: rgba(255, 255, 255, 0.54);
}
.iconInput + &:hover {
opacity: 1 !important;
&.main::after {
background-color: #fff;
}
.iconInput + &:hover::after {
background-color: #fff;
}
.iconInput:checked + & {
opacity: 1;
&:after {
background: var(--theme);
}
@@ -42,31 +45,82 @@
}
}
.button {
.iconButton {
-webkit-appearance: none;
width: 24px;
height: 24px;
display: block;
cursor: pointer;
border: 0;
opacity: 0.54;
background: transparent;
padding: 0;
margin: 0;
&:hover {
opacity: 1;
&::after {
background-color: rgba(255, 255, 255, 0.54);
}
&:hover::after {
background-color: #fff;
}
}
.group {
display: flex;
gap: 8px;
padding: 4px 0;
}
.label {
flex-basis: 40%;
flex-grow: 0;
flex-shrink: 0;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
}
.select,
.input {
.input,
.button {
background: #404040;
color: rgba(255, 255, 255, 0.6);
border: 0;
border-radius: 4px;
padding: 0 8px;
font-size: 14px;
height: 24px;
flex-shrink: 1;
flex-grow: 1;
flex-basis: 100%;
min-width: 0;
&:hover {
background-color: #333;
}
&.main {
background-color: var(--theme);
color: rgba(0, 0, 0, 0.87);
font-weight: 700;
&:hover {
box-shadow: 0 0 0 2px white inset;
}
}
}
.input[type='number'] {
text-align: right;
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
-webkit-appearance: none;
}
}
.select,
.button {
cursor: pointer;
}
.pause:after {

View File

@@ -0,0 +1,12 @@
import styles from './Controls.module.scss';
import type {JSX} from 'preact';
interface GroupProps extends JSX.HTMLAttributes<HTMLDivElement> {}
export function Group(props: GroupProps) {
return (
<div className={styles.group} {...props}>
{props.children}
</div>
);
}

View File

@@ -11,7 +11,7 @@ interface IconButtonProps {
export function IconButton({icon, onClick}: IconButtonProps) {
return (
<button
className={classes(styles.button, styles.icon, styles[icon])}
className={classes(styles.iconButton, styles.icon, styles[icon])}
type="button"
onClick={onClick}
/>

View File

@@ -1,5 +1,4 @@
import styles from './Controls.module.scss';
import type {JSX} from 'preact';
interface InputProps extends JSX.HTMLAttributes<HTMLInputElement> {}

View File

@@ -0,0 +1,8 @@
import styles from './Controls.module.scss';
import type {JSX} from 'preact';
interface LabelProps extends JSX.HTMLAttributes<HTMLLabelElement> {}
export function Label(props: LabelProps) {
return <label className={styles.label} {...props} />;
}

View File

@@ -1,4 +1,9 @@
export * from "./Button";
export * from "./Group";
export * from "./Input";
export * from "./Icon";
export * from './IconButton';
export * from './IconCheckbox';
export * from "./IconType";
export * from "./IconType";
export * from "./Label";
export * from "./Select";

View File

@@ -6,6 +6,16 @@
&.vertical {
flex-direction: column;
> .left {
max-height: 100%;
}
}
&:not(.vertical) {
> .left {
max-width: 100%;
}
}
}

View File

@@ -18,7 +18,7 @@ export function ResizeableLayout({
start,
end,
vertical = false,
size = 540,
size = 0.3,
id = null,
resizeable = true,
}: ResizeableLayoutProps) {
@@ -29,7 +29,9 @@ export function ResizeableLayout({
useCallback(
(dx, dy, x, y) => {
const rect = containerRef.current.getBoundingClientRect();
setSize(vertical ? y - rect.y : x - rect.x);
setSize(
vertical ? (y - rect.y) / rect.height : (x - rect.x) / rect.width,
);
},
[vertical, setSize],
),
@@ -38,19 +40,20 @@ export function ResizeableLayout({
const style = useMemo<JSX.CSSProperties>(() => {
if (!resizeable) return {};
return vertical
? {height: `${currentSize}px`}
: {width: `${currentSize}px`};
? {height: `${currentSize * 100}%`}
: {width: `${currentSize * 100}%`};
}, [currentSize, vertical, resizeable]);
return (
<div
ref={containerRef}
className={classes(
styles.root,
[styles.vertical, vertical],
[styles.resizeable, resizeable],
)}
>
<div ref={containerRef} className={styles.left} style={style}>
<div className={styles.left} style={style}>
{start}
</div>
<div onMouseDown={handleDrag} className={styles.separator} />

View File

@@ -12,4 +12,9 @@
display: flex;
justify-content: center;
gap: 16px;
&.disabled {
opacity: 0.54;
pointer-events: none;
}
}

View File

@@ -2,10 +2,10 @@ import styles from './Playback.module.scss';
import {IconType, IconButton, IconCheckbox} from '../controls';
import {useDocumentEvent, usePlayer, usePlayerState} from '../../hooks';
import {Select} from '../controls/Select';
import {Input} from '../controls/Input';
import {Select, Input} from '../controls';
import {Framerate} from './Framerate';
import {useCallback} from 'preact/hooks';
import {classes} from '../../utils';
export function PlaybackControls() {
const player = usePlayer();
@@ -26,7 +26,7 @@ export function PlaybackControls() {
player.requestNextFrame();
break;
case 'm':
player.updateState({muted: !state.muted});
player.toggleAudio();
break;
case 'l':
player.updateState({loop: !state.loop});
@@ -38,7 +38,7 @@ export function PlaybackControls() {
);
return (
<div className={styles.controls}>
<div className={classes(styles.controls, [styles.disabled, state.render])}>
<Select
options={[
{value: 0.25, text: 'x0.25'},

View File

@@ -4,9 +4,10 @@ import type {PlayerRenderEvent} from '@motion-canvas/core/player/Player';
import {IconType} from '../controls';
import {Tabs} from '../tabs/Tabs';
import {usePlayer, usePlayerState} from '../../hooks';
import {useCallback, useEffect, useState} from 'preact/hooks';
import {useCallback, useEffect, useMemo, useState} from 'preact/hooks';
import {Thread} from '@motion-canvas/core/threading';
import {GeneratorHelper} from '@motion-canvas/core/helpers';
import {Button, Label, Input, Group, Select} from '../controls';
interface SidebarProps {
setOpen?: (value: boolean) => any;
@@ -17,7 +18,7 @@ export function Sidebar({setOpen}: SidebarProps) {
<Tabs onToggle={tab => setOpen(tab >= 0)} id="sidebar">
{{
icon: IconType.tune,
pane: <div className={styles.pane}>Settings</div>,
pane: <Properties />,
}}
{{
icon: IconType.videoSettings,
@@ -31,9 +32,26 @@ export function Sidebar({setOpen}: SidebarProps) {
);
}
function Properties() {
return (
<div className={styles.pane}>
<div className={styles.header}>Properties</div>
</div>
);
}
function Rendering() {
const player = usePlayer();
const state = usePlayerState();
const {width, height} = player.project.getSize();
const resolutions = useMemo(() => {
return [
{value: 0.5, text: `${width / 2}x${height / 2} (Half)`},
{value: 1, text: `${width}x${height} (Full)`},
{value: 2, text: `${width * 2}x${height * 2} (Double)`},
];
}, [width, height]);
const handleRender = useCallback(async ({frame, data}: PlayerRenderEvent) => {
try {
@@ -56,9 +74,57 @@ function Rendering() {
return (
<div className={styles.pane}>
<div className={styles.header}>Rendering</div>
<button onClick={() => player.toggleRendering()}>
{state.render ? 'RENDERING...' : 'RENDER'}
</button>
<Group>
<Label>Range</Label>
<Input
min={0}
max={state.endFrame}
type={'number'}
value={state.startFrame}
onChange={event => {
player.updateState({
startFrame: parseInt((event.target as HTMLInputElement).value),
});
}}
/>
<Input
min={state.startFrame}
max={state.duration}
type={'number'}
value={Math.min(state.duration, state.endFrame)}
onChange={event => {
const value = parseInt((event.target as HTMLInputElement).value);
player.updateState({
endFrame: value >= state.duration ? Infinity : value,
});
}}
/>
</Group>
<Group>
<Label>FPS</Label>
<Select
options={[
{value: 30, text: '30 FPS'},
{value: 60, text: '60 FPS'},
]}
value={state.fps}
onChange={value => player.setFramerate(value)}
/>
</Group>
<Group>
<Label>Resolution</Label>
<Select
options={resolutions}
value={state.scale}
onChange={value => player.setScale(value)}
/>
</Group>
<Group>
<Label />
<Button main onClick={() => player.toggleRendering()}>
{state.render ? 'STOP RENDERING' : 'RENDER'}
</Button>
</Group>
</div>
);
}

View File

@@ -14,12 +14,17 @@ export function RangeTrack() {
const [end, setEnd] = useState(state.endFrame);
const onDrop = useCallback(() => {
player.updateState({
startFrame: Math.max(0, Math.floor(start)),
endFrame: end >= state.duration
let startFrame = Math.max(0, Math.floor(start));
let endFrame =
end >= state.duration
? Infinity
: Math.min(state.duration, Math.floor(end)),
});
: Math.min(state.duration, Math.floor(end));
if (startFrame > endFrame) {
[startFrame, endFrame] = [endFrame, startFrame];
}
player.updateState({startFrame, endFrame});
}, [start, end, state.duration]);
const [handleDragStart] = useDrag(
@@ -27,7 +32,7 @@ export function RangeTrack() {
dx => {
setStart(start + (dx / fullLength) * state.duration);
},
[start, setStart, fullLength, state.duration],
[start, fullLength, state.duration],
),
onDrop,
);
@@ -39,7 +44,7 @@ export function RangeTrack() {
Math.min(state.duration, end) + (dx / fullLength) * state.duration,
);
},
[end, setEnd, fullLength, state.duration],
[end, fullLength, state.duration],
),
onDrop,
);
@@ -52,7 +57,7 @@ export function RangeTrack() {
Math.min(state.duration, end) + (dx / fullLength) * state.duration,
);
},
[start, end, fullLength, state.duration, setStart, setEnd],
[start, end, fullLength, state.duration],
),
onDrop,
);
@@ -62,11 +67,19 @@ export function RangeTrack() {
setEnd(state.endFrame);
}, [state.startFrame, state.endFrame]);
let normalizedStart = start;
let normalizedEnd = end;
if (start > end) {
normalizedStart = end;
normalizedEnd = start;
}
return (
<div
style={{
left: `${(Math.max(0, start) / state.duration) * 100}%`,
right: `${100 - Math.min(1, (end + 1) / state.duration) * 100}%`,
flexDirection: start > end ? 'row-reverse' : 'row',
left: `${(Math.max(0, normalizedStart) / state.duration) * 100}%`,
right: `${100 - Math.min(1, (normalizedEnd + 1) / state.duration) * 100}%`,
}}
className={styles.range}
onMouseDown={event => {

View File

@@ -216,12 +216,17 @@
border-bottom: 1px solid var(--theme);
display: flex;
align-items: center;
justify-content: space-between;
justify-content: center;
}
.handle {
cursor: pointer;
padding: 8px 2px;
&:first-child {
margin-right: auto;
}
&:after {
background-color: rgba(255, 255, 255, 0);
}