feat(ui): visual changes (#96)

This commit is contained in:
Jacob
2022-12-06 15:08:01 +01:00
committed by GitHub
parent be83edf847
commit 3d599f4e17
14 changed files with 139 additions and 28 deletions

View File

@@ -113,6 +113,26 @@
}
}
.inputSelect {
display: flex;
flex-grow: 1;
flex-shrink: 1;
.input {
width: 0;
border-radius: 4px 0 0 4px;
border-right: 1px solid var(--surface-color);
}
.select {
font-size: 0;
flex-grow: 0;
flex-basis: 0;
padding-left: 12px;
border-radius: 0 4px 4px 0;
}
}
.input[type='number'] {
text-align: right;
&::-webkit-inner-spin-button,
@@ -243,3 +263,9 @@
.locate:after {
-webkit-mask-image: url('../../img/icons/locate.svg');
}
.add:after {
-webkit-mask-image: url('../../img/icons/add.svg');
}
.unfoldMore:after {
-webkit-mask-image: url('../../img/icons/unfold_more.svg');
}

View File

@@ -16,4 +16,6 @@ export enum IconType {
bug = 'bug',
clear = 'clear',
locate = 'locate',
add = 'add',
unfoldMore = 'unfoldMore',
}

View File

@@ -0,0 +1,30 @@
import styles from './Controls.module.scss';
import type {JSX} from 'preact';
import {Input} from './Input';
import {Select, SelectProps} from './Select';
export type InputSelect<T> = Omit<
JSX.HTMLAttributes<HTMLInputElement>,
'value' | 'onChange'
> &
SelectProps<T>;
export function InputSelect<T extends string | number>({
options,
value,
onChange,
...props
}: InputSelect<T>) {
return (
<div className={styles.inputSelect}>
<Input
value={value}
onChange={event => {
onChange((event.target as HTMLInputElement).value as T);
}}
{...props}
/>
<Select value={value} options={options} onChange={onChange} />
</div>
);
}

View File

@@ -1,17 +1,25 @@
import styles from './Controls.module.scss';
import {classes} from '../../utils';
interface SelectProps<T> {
export interface SelectProps<T> {
title?: string;
options: {value: T; text: string}[];
className?: string;
value: T;
onChange: (value: T) => void;
}
export function Select<T>({options, value, onChange, title}: SelectProps<T>) {
export function Select<T>({
options,
value,
onChange,
title,
className,
}: SelectProps<T>) {
return (
<select
title={title}
className={styles.select}
className={classes(styles.select, className)}
value={options.findIndex(option => option.value === value)}
onChange={event =>
onChange(

View File

@@ -1,4 +1,5 @@
export * from './Button';
export * from './InputSelect';
export * from './Group';
export * from './Input';
export * from './Icon';

View File

@@ -1,5 +1,16 @@
import styles from './Sidebar.module.scss';
import {usePlayerState} from '../../hooks';
import {Button, Group, Input, Label, Select} from '../controls';
import {
Button,
Group,
Icon,
IconType,
Input,
InputSelect,
Label,
Select,
} from '../controls';
import {Pane} from '../tabs';
import {usePlayer} from '../../contexts';
import type {
@@ -29,6 +40,11 @@ export function Rendering() {
{value: 'image/webp', text: 'webp'},
];
const frameRates = [
{value: '30', text: '30 FPS'},
{value: '60', text: '60 FPS'},
];
return (
<Pane title="Rendering">
<Group>
@@ -55,14 +71,14 @@ export function Rendering() {
</Group>
<Group>
<Label>frame rate</Label>
<Input
<InputSelect
type="number"
min={1}
value={state.fps}
onChange={event => {
const value = parseInt((event.target as HTMLInputElement).value);
player.setFramerate(value);
value={state.fps.toString()}
onChange={value => {
player.setFramerate(parseInt(value));
}}
options={frameRates}
/>
</Group>
<Group>
@@ -77,7 +93,7 @@ export function Rendering() {
player.project.setSize(value, height);
}}
/>
x
<Icon className={styles.times} type={IconType.add} />
<Input
type="number"
min={1}
@@ -113,19 +129,23 @@ export function Rendering() {
onChange={value => player.setFileType(value as CanvasOutputMimeType)}
/>
</Group>
<Group>
<Label>quality</Label>
<Input
type="number"
min={0}
max={1}
value={state.quality}
onChange={event => {
const value = parseFloat((event.target as HTMLInputElement).value);
player.setQuality(value);
}}
/>
</Group>
{state.fileType !== 'image/png' && (
<Group>
<Label>quality (%)</Label>
<Input
type="number"
min={0}
max={100}
value={state.quality * 100}
onChange={event => {
const value = parseFloat(
(event.target as HTMLInputElement).value,
);
player.setQuality(value / 100);
}}
/>
</Group>
)}
<Group>
<Label />
<Button main onClick={() => player.toggleRendering()}>

View File

@@ -2,6 +2,12 @@
background-color: var(--surface-color);
}
.times {
rotate: 45deg;
--icon-color: rgba(255, 255, 255, 0.32);
margin: 0 -4px;
}
.thread {
padding: 4px 0;
}

View File

@@ -35,7 +35,7 @@ export function Timeline() {
const player = usePlayer();
const containerRef = useRef<HTMLDivElement>();
const playheadRef = useRef<HTMLDivElement>();
const {duration} = usePlayerState();
const {duration, fps} = usePlayerState();
const rect = useSize(containerRef);
const [offset, setOffset] = useState(0);
const [scale, setScale] = useState(1);
@@ -95,9 +95,10 @@ export function Timeline() {
useStateChange(
([prevDuration, prevWidth]) => {
const newDuration = duration / fps;
let newScale = scale;
if (prevDuration !== 0 && duration !== 0) {
newScale *= duration / prevDuration;
if (prevDuration !== 0 && newDuration !== 0) {
newScale *= newDuration / prevDuration;
}
if (prevWidth !== 0 && rect.width !== 0) {
newScale *= prevWidth / rect.width;
@@ -106,7 +107,7 @@ export function Timeline() {
setScale(clamp(ZOOM_MIN, ZOOM_MAX, newScale));
}
},
[duration, rect.width],
[duration / fps, rect.width],
);
useDocumentEvent(

View File

@@ -39,7 +39,7 @@ export function Debug() {
ctx.save();
scene.drawOverlay(element, matrix, ctx);
ctx.restore();
}, [state, scene, inspectedElement, time]);
}, [state, scene, inspectedElement, time, scale]);
return (
<canvas

View File

@@ -8,6 +8,7 @@ import {
useStorage,
useSubscribable,
usePlayerState,
useStateChange,
} from '../../hooks';
import {Debug} from './Debug';
import {Grid} from './Grid';
@@ -64,6 +65,17 @@ export function View() {
player.project.setCanvas(viewportRef.current);
}, [playerState.colorSpace]);
useStateChange(
([scale]) => {
console.log(playerState.scale);
const zoom = (state.zoom * scale) / playerState.scale;
if (!isNaN(zoom) && zoom > 0) {
setState({...state, zoom});
}
},
[playerState.scale],
);
useSubscribable(
player.onReloaded,
() => overlayRef.current.animate(highlight(), {duration: 300}),

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 5.83L15.17 9l1.41-1.41L12 3 7.41 7.59 8.83 9 12 5.83zm0 12.34L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15 12 18.17z"/></svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@@ -24,6 +24,7 @@
box-sizing: border-box;
font-family: 'JetBrains Mono', sans-serif;
font-size: 14px;
accent-color: var(--theme);
&::-webkit-scrollbar {
width: 16px;

View File

@@ -19,6 +19,8 @@ function renderRoot(vnode: ComponentChild) {
}
export function editor(project: Project) {
Error.stackTraceLimit = Infinity;
project.logger.onLogged.subscribe(log => {
const {level, message, stack, object, durationMs, ...rest} = log;
const fn = console[level as 'error'] ?? console.log;