mirror of
https://github.com/motion-canvas/motion-canvas.git
synced 2026-01-12 07:18:01 -05:00
feat(ui): visual changes (#96)
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -16,4 +16,6 @@ export enum IconType {
|
||||
bug = 'bug',
|
||||
clear = 'clear',
|
||||
locate = 'locate',
|
||||
add = 'add',
|
||||
unfoldMore = 'unfoldMore',
|
||||
}
|
||||
|
||||
30
packages/ui/src/components/controls/InputSelect.tsx
Normal file
30
packages/ui/src/components/controls/InputSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './Button';
|
||||
export * from './InputSelect';
|
||||
export * from './Group';
|
||||
export * from './Input';
|
||||
export * from './Icon';
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}),
|
||||
|
||||
1
packages/ui/src/img/icons/add.svg
Normal file
1
packages/ui/src/img/icons/add.svg
Normal 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 |
1
packages/ui/src/img/icons/unfold_more.svg
Normal file
1
packages/ui/src/img/icons/unfold_more.svg
Normal 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 |
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user