Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions app/src/actions/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,36 @@ export const useUpdateValueEvents = (type: ValueEventType) => {
)
}

export const useUpdateValueEventsWithCurve = (type: ValueEventType) => {
const { selectedTrackId } = usePianoRoll()
const { quantizeFloor, quantizeUnit } = useQuantizer()
const commands = useCommands()

return useCallback(
(
startValue: number,
endValue: number,
startTick: number,
endTick: number,
easing: (t: number) => number,
) => {
commands.track.updateEventsInRangeWithEasing(
selectedTrackId,
ValueEventType.getEventPredicate(type),
ValueEventType.getEventFactory(type),
quantizeFloor,
quantizeUnit,
startValue,
endValue,
startTick,
endTick,
easing,
)
},
[commands, selectedTrackId, type, quantizeFloor, quantizeUnit],
)
}

/* note */

export const useMuteNote = () => {
Expand Down
24 changes: 19 additions & 5 deletions app/src/components/ControlPane/ControlPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { usePianoRoll } from "../../hooks/usePianoRoll"
import { useRootView } from "../../hooks/useRootView"
import { ControlName } from "./ControlName"
import { ValueEventGraph } from "./Graph/ValueEventGraph"
import { PencilModeSelector } from "./PencilModeSelector"
import PianoVelocityControl from "./VelocityControl/VelocityControl"

interface TabBarProps {
Expand All @@ -38,7 +39,7 @@ const TabButtonBase = styled.div`
`

const TabButton = styled(TabButtonBase)`
width: 7rem;
min-width: 4rem;
overflow: hidden;
border-bottom: 1px solid;
border-color: transparent;
Expand Down Expand Up @@ -97,7 +98,7 @@ const Parent = styled.div`
height: 100%;
display: flex;
flex-direction: column;
background: var(--color-background-dark);
background: var(--color-background);
outline: none;
`

Expand All @@ -115,9 +116,16 @@ const Content = styled.div`

const TabBarWrapper = styled.div`
position: relative;
display: flex;
align-items: stretch;
`

const TAB_HEIGHT = 30
const TabBarScrollArea = styled.div`
flex: 1;
overflow: hidden;
`

const TAB_HEIGHT = 32
const BORDER_WIDTH = 1

export interface ControlPaneProps {
Expand All @@ -127,7 +135,7 @@ export interface ControlPaneProps {
const ControlPane: FC<ControlPaneProps> = ({ axisWidth }) => {
const ref = useRef(null)
const containerSize = useComponentSize(ref)
const { setActivePane } = usePianoRoll()
const { setActivePane, mouseMode } = usePianoRoll()
const { controlMode: mode, setControlMode } = useControlPane()
const keyboardShortcutProps = useControlPaneKeyboardShortcut()

Expand All @@ -149,6 +157,9 @@ const ControlPane: FC<ControlPaneProps> = ({ axisWidth }) => {
}
})()

const showPencilModeSelector =
mouseMode === "pencil" && mode.type !== "velocity"

return (
<Parent
ref={ref}
Expand All @@ -158,7 +169,10 @@ const ControlPane: FC<ControlPaneProps> = ({ axisWidth }) => {
onBlur={onBlur}
>
<TabBarWrapper style={{ paddingLeft: axisWidth }}>
<TabBar onSelect={setControlMode} selectedMode={mode} />
<TabBarScrollArea>
<TabBar onSelect={setControlMode} selectedMode={mode} />
</TabBarScrollArea>
{showPencilModeSelector && <PencilModeSelector />}
</TabBarWrapper>
<Content>{control}</Content>
</Parent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useState } from "react"
import { useUpdateValueEventsWithCurve } from "../../../../actions"
import { ValueEventType } from "../../../../entities/event/ValueEventType"
import { Point } from "../../../../entities/geometry/Point"
import { ControlCoordTransform } from "../../../../entities/transform/ControlCoordTransform"
import { MouseGesture } from "../../../../gesture/MouseGesture"
import { getClientPos } from "../../../../helpers/mouseEvent"
import { observeDrag } from "../../../../helpers/observeDrag"
import { useControlPane } from "../../../../hooks/useControlPane"
import { useHistory } from "../../../../hooks/useHistory"
import { usePianoRoll } from "../../../../hooks/usePianoRoll"

export type CurveDragState = { start: Point; end: Point }

export type CurveType = "linear" | "easeIn" | "easeOut"
export const curveTypes = ["easeIn", "easeOut"] as CurveType[]

export const curveEasings: Record<CurveType, (t: number) => number> = {
linear: (t) => t,
easeIn: (t) => (t * t + 1 - Math.cos((t * Math.PI) / 2)) / 2,
easeOut: (t) => (Math.sin((t * Math.PI) / 2) + t * (2 - t)) / 2,
}

export const useCurveGesture = (type: ValueEventType, curveType: CurveType) => {
const { setSelection: setPianoRollSelection, setSelectedNoteIds } =
usePianoRoll()
const { setSelectedEventIds, setSelection } = useControlPane()
const { pushHistory } = useHistory()
const updateValueEvents = useUpdateValueEventsWithCurve(type)
const [curveDragState, setCurveDragState] = useState<CurveDragState | null>(
null,
)

const gesture: MouseGesture<[Point, ControlCoordTransform]> = {
onMouseDown(e, startPoint, transform) {
pushHistory()

setSelectedEventIds([])
setSelection(null)
setPianoRollSelection(null)
setSelectedNoteIds([])

const startClientPos = getClientPos(e)
setCurveDragState({ start: startPoint, end: startPoint })

observeDrag({
onMouseMove(e) {
const posPx = getClientPos(e)
const deltaPx = Point.sub(posPx, startClientPos)
const endPoint = Point.add(startPoint, deltaPx)
setCurveDragState({ start: startPoint, end: endPoint })
},
onMouseUp(e) {
const posPx = getClientPos(e)
const deltaPx = Point.sub(posPx, startClientPos)
const endPoint = Point.add(startPoint, deltaPx)

const startPos = transform.fromPosition(startPoint)
const endValue = Math.max(
0,
Math.min(
transform.maxValue,
transform.fromPosition(endPoint).value,
),
)
const endTick = transform.getTick(endPoint.x)

updateValueEvents(
startPos.value,
endValue,
startPos.tick,
endTick,
curveEasings[curveType],
)
setCurveDragState(null)
},
})
},
}

return { gesture, curveDragState }
}
63 changes: 63 additions & 0 deletions app/src/components/ControlPane/LineGraph/DragPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CSSProperties, useMemo } from "react"
import { type Point } from "../../../entities/geometry/Point"

export interface DragPreviewProps {
start: Point
end: Point
scrollLeft: number
width: number
height: number
color: string
easing: (t: number) => number
}

const NUM_SAMPLES = 32

export const DragPreview = ({
start,
end,
scrollLeft,
width,
height,
color,
easing,
}: DragPreviewProps) => {
const points = useMemo(() => {
const sx = start.x
const sy = start.y
const ex = end.x
const ey = end.y
return Array.from({ length: NUM_SAMPLES + 1 }, (_, i) => {
const t = i / NUM_SAMPLES
const x = sx + t * (ex - sx)
const y = sy + easing(t) * (ey - sy)
return `${x},${y}`
}).join(" ")
}, [start.x, start.y, end.x, end.y, easing])

const style: CSSProperties = useMemo(
() => ({
position: "absolute",
top: 0,
left: 0,
pointerEvents: "none",
}),
[],
)

return (
<svg style={style} width={width} height={height} aria-hidden="true">
<g transform={`translate(${-scrollLeft}, 0)`}>
<polyline
points={points}
fill="none"
stroke={color}
strokeWidth={2}
strokeDasharray="6 3"
/>
<circle cx={start.x} cy={start.y} r={4} fill={color} />
<circle cx={end.x} cy={end.y} r={4} fill={color} />
</g>
</svg>
)
}
Loading