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
2 changes: 2 additions & 0 deletions .yarn/versions/69337c4c.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
beatmapper: minor
98 changes: 61 additions & 37 deletions src/components/app/templates/events/basic-track.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,59 +10,100 @@ import { resolveColorForItem } from "$/helpers/colors.helpers";
import { isBasicLightEvent, isBasicValueEvent, resolveBasicEventColor, resolveBasicEventEffect, resolveEventId, serializeBasicEventValue } from "$/helpers/events.helpers";
import { addBasicEvent, bulkAddBasicEvent, bulkRemoveEvent, deselectEvent, mirrorBasicEvent, removeEvent, selectEvent, updateBasicEvent } from "$/store/actions";
import { useAppDispatch, useAppSelector } from "$/store/hooks";
import { selectAllBasicEventsForTrackInWindow, selectColorScheme, selectCurrentLightStateForTrack, selectEditorOffsetInBeats, selectEventEditorStartAndEndBeat, selectEventsEditorColor, selectEventsEditorMirrorLock, selectEventsEditorTool, selectEventTracksForEnvironment } from "$/store/selectors";
import {
selectAllBasicEventsForTrack,
selectAllBoostEvents,
selectColorScheme,
selectCurrentLightStateForTrack,
selectEditorOffsetInBeats,
selectEventEditorStartAndEndBeat,
selectEventsEditorColor,
selectEventsEditorMirrorLock,
selectEventsEditorTool,
selectEventTracksForEnvironment,
selectToggleAtBeat,
} from "$/store/selectors";
import { App, type IEventTracks, TrackType } from "$/types";
import { clamp, isColorDark, normalize } from "$/utils";
import { createBackgroundBoxes } from "./track.helpers";
import { createBackgroundBoxes, resolveColorForLightState } from "./track.helpers";

function resolveBackgroundForEvent(data: wrapper.IWrapBasicEvent, options: Parameters<typeof resolveColorForItem>[1] & { tracks: IEventTracks }) {
const eventColor = resolveBasicEventColor(data);
const eventEffect = resolveBasicEventEffect(data, options.tracks);
function resolveBackgroundForEvent(data: wrapper.IWrapBasicEvent, options: Parameters<typeof resolveColorForItem>[1] & { isBoosted: boolean; tracks: IEventTracks }) {
const effect = resolveBasicEventEffect(data, options.tracks);

const color = resolveColorForItem(isBasicLightEvent(data, options.tracks) ? (eventColor ?? eventEffect) : eventEffect, options);
const key = resolveColorForLightState({ color: resolveBasicEventColor(data), isBoosted: options.isBoosted }, options);
const color = isBasicLightEvent(data, options.tracks) ? (key ?? resolveColorForItem(effect, options)) : resolveColorForItem(effect, options);

const brightColor = `color-mix(in srgb, ${color}, white 30%)`;
const semiTransparentColor = `color-mix(in srgb, ${color}, black 30%)`;
const toWhite = `color-mix(in srgb, ${color}, white 30%)`;
const toBlack = `color-mix(in srgb, ${color}, black 30%)`;

switch (eventEffect) {
switch (effect) {
case App.BasicEventEffect.ON: {
return { value: color, style: color };
}
case App.BasicEventEffect.FLASH: {
return { value: color, style: `linear-gradient(90deg, ${semiTransparentColor}, ${brightColor})` };
return { value: color, style: `linear-gradient(90deg, ${toBlack}, ${toWhite})` };
}
case App.BasicEventEffect.FADE: {
return { value: color, style: `linear-gradient(-90deg, ${semiTransparentColor}, ${brightColor})` };
return { value: color, style: `linear-gradient(-90deg, ${toBlack}, ${toWhite})` };
}
case App.BasicEventEffect.TRANSITION: {
return { value: color, style: `linear-gradient(0deg, ${semiTransparentColor}, ${brightColor})` };
return { value: color, style: `linear-gradient(0deg, ${toBlack}, ${toWhite})` };
}
default: {
return { value: color, style: `linear-gradient(90deg, ${semiTransparentColor}, ${brightColor}, ${semiTransparentColor})` };
return { value: color, style: `linear-gradient(90deg, ${toBlack}, ${toWhite}, ${toBlack})` };
}
}
}

function BasicEvent({ data, actions }: { data: App.IBasicEvent; actions: EventGrid.IPlacementActions<wrapper.IWrapBasicEvent> }) {
const { sid, bid } = useParams({ from: "/_/edit/$sid/$bid/_" });

const tracks = useAppSelector((state) => selectEventTracksForEnvironment(state, sid, bid));
const colorScheme = useAppSelector((state) => selectColorScheme(state, sid, bid));

const api = EventGrid.useContext();

const isEventBoosted = useAppSelector((state) => selectToggleAtBeat(state, { trackId: 5, beforeBeat: data.time + 0.001 }));

const resolveEventStyle = useCallback(
(data: wrapper.IWrapBasicEvent) => {
const { style, value } = resolveBackgroundForEvent(data, { tracks, colorScheme, isBoosted: isEventBoosted });
return { "--event-color": style, background: style, color: isColorDark(value) ? "white" : "black" };
},
[tracks, colorScheme, isEventBoosted],
);

return (
<EventGrid.Event key={resolveEventId(data)} data={data} {...api.getEventProps(data, actions, resolveEventStyle(data))}>
{isBasicLightEvent(data, tracks) && data.value !== 0 ? data.floatValue : undefined}
{isBasicValueEvent(data, tracks) && data.value}
</EventGrid.Event>
);
}

interface Props {
trackId: number;
}
function BasicEventTrack({ trackId, ...rest }: Assign<ComponentProps<typeof EventGrid.Track>, Props>) {
const { sid, bid } = useParams({ from: "/_/edit/$sid/$bid/_" });

const dispatch = useAppDispatch();
const selectedTool = useAppSelector(selectEventsEditorTool);
const selectedColorType = useAppSelector(selectEventsEditorColor);
const areLasersLocked = useAppSelector(selectEventsEditorMirrorLock);
const { startBeat, endBeat } = useAppSelector((state) => selectEventEditorStartAndEndBeat(state, sid));
const tracks = useAppSelector((state) => selectEventTracksForEnvironment(state, sid, bid));
const basicEvents = useAppSelector((state) => selectAllBasicEventsForTrackInWindow(state, sid, trackId));
const basicEvents = useAppSelector((state) => selectAllBasicEventsForTrack(state, trackId));
const boostEvents = useAppSelector((state) => selectAllBoostEvents(state));
const colorScheme = useAppSelector((state) => selectColorScheme(state, sid, bid));
const selectedTool = useAppSelector(selectEventsEditorTool);
const selectedColorType = useAppSelector(selectEventsEditorColor);
const initialLightState = useAppSelector((state) => selectCurrentLightStateForTrack(state, sid, bid, trackId));
const offsetInBeats = useAppSelector((state) => selectEditorOffsetInBeats(state, sid));
const areLasersLocked = useAppSelector(selectEventsEditorMirrorLock);

const backgroundBoxes = useMemo(() => {
return createBackgroundBoxes(trackId, { tracks, colorScheme, offsetInBeats, basicEvents, initialLightState, startBeat, endBeat });
}, [initialLightState, trackId, tracks, colorScheme, offsetInBeats, basicEvents, startBeat, endBeat]);
return createBackgroundBoxes(trackId, { tracks, colorScheme, offsetInBeats, basicEvents, boostEvents, initialLightState, startBeat, endBeat });
}, [initialLightState, trackId, tracks, colorScheme, offsetInBeats, basicEvents, boostEvents, startBeat, endBeat]);

const api = EventGrid.useContext();

const resolveEventData = useCallback(
(time: number, norm: number) => {
Expand All @@ -88,8 +129,6 @@ function BasicEventTrack({ trackId, ...rest }: Assign<ComponentProps<typeof Even
[tracks, selectedColorType, selectedTool, trackId],
);

const api = EventGrid.useContext();

const actions = useMemo<EventGrid.IPlacementActions<wrapper.IWrapBasicEvent>>(() => {
return {
onCreate: resolveEventData,
Expand Down Expand Up @@ -118,25 +157,10 @@ function BasicEventTrack({ trackId, ...rest }: Assign<ComponentProps<typeof Even
};
}, [dispatch, resolveEventData, trackId, tracks, areLasersLocked]);

const resolveEventStyle = useCallback(
(data: wrapper.IWrapBasicEvent) => {
const { style, value } = resolveBackgroundForEvent(data, { tracks, colorScheme });
return { "--event-color": style, background: style, color: isColorDark(value) ? "white" : "black" };
},
[tracks, colorScheme],
);

return (
<EventGrid.Track {...api.getTrackProps(trackId, actions)} {...rest}>
<For each={backgroundBoxes}>{(box) => <EventGrid.BackgroundBox key={resolveEventId({ type: trackId, time: box.time })} {...api.getBackgroundBoxProps(box)} />}</For>
<For each={basicEvents.filter((x) => x.time >= startBeat && x.time < endBeat)}>
{(data) => (
<EventGrid.Event key={resolveEventId(data)} data={data} {...api.getEventProps(data, actions, resolveEventStyle(data))}>
{isBasicLightEvent(data, tracks) && data.value !== 0 ? data.floatValue : undefined}
{isBasicValueEvent(data, tracks) && data.value}
</EventGrid.Event>
)}
</For>
<For each={basicEvents.filter((x) => x.time >= startBeat && x.time < endBeat)}>{(data) => <BasicEvent data={data} actions={actions} />}</For>
</EventGrid.Track>
);
}
Expand Down
78 changes: 78 additions & 0 deletions src/components/app/templates/events/boost-track.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { Assign } from "@ark-ui/react";
import { useParams } from "@tanstack/react-router";
import { createColorBoostEvent } from "bsmap";
import type { wrapper } from "bsmap/types";
import { type ComponentProps, useCallback, useMemo } from "react";

import { EventGrid } from "$/components/app/layouts";
import { For } from "$/components/ui/atoms";
import { resolveEventId } from "$/helpers/events.helpers";
import { addBoostEvent, bulkAddBoostEvent, bulkRemoveEvent, deselectEvent, removeEvent, selectEvent, updateBoostEvent } from "$/store/actions";
import { useAppDispatch, useAppSelector } from "$/store/hooks";
import { selectAllBoostEvents, selectEventEditorStartAndEndBeat, selectEventsEditorMirrorLock, selectEventTracksForEnvironment } from "$/store/selectors";
import type { App } from "$/types";
import { isColorDark } from "$/utils";
import { token } from "$:styled-system/tokens";

function BoostEvent({ data, actions }: { data: App.IBoostEvent; actions: EventGrid.IPlacementActions<wrapper.IWrapColorBoostEvent> }) {
const api = EventGrid.useContext();

const color = token("colors.pink.500");

const resolveEventStyle = useCallback(
(_: wrapper.IWrapColorBoostEvent) => {
const toWhite = `color-mix(in srgb, ${color}, white 30%)`;
const toBlack = `color-mix(in srgb, ${color}, black 30%)`;

const style = `radial-gradient(${toBlack}, ${toWhite})`;

return { "--event-color": style, background: style, color: isColorDark(color) ? "white" : "black" };
},
[color],
);

return (
<EventGrid.Event key={resolveEventId(data)} data={data} {...api.getEventProps(data, actions, resolveEventStyle(data))}>
{data.toggle ? "1" : "0"}
</EventGrid.Event>
);
}

interface Props {
trackId: number;
}
function BoostEventTrack({ trackId, ...rest }: Assign<ComponentProps<typeof EventGrid.Track>, Props>) {
const { sid, bid } = useParams({ from: "/_/edit/$sid/$bid/_" });

const dispatch = useAppDispatch();
const areLasersLocked = useAppSelector(selectEventsEditorMirrorLock);
const { startBeat, endBeat } = useAppSelector((state) => selectEventEditorStartAndEndBeat(state, sid));
const tracks = useAppSelector((state) => selectEventTracksForEnvironment(state, sid, bid));
const boostEvents = useAppSelector((state) => selectAllBoostEvents(state));

const api = EventGrid.useContext();

const resolveEventData = useCallback((time: number, norm: number) => createColorBoostEvent({ time, toggle: norm <= 0.5 }), []);

const actions = useMemo<EventGrid.IPlacementActions<wrapper.IWrapColorBoostEvent>>(() => {
return {
onCreate: resolveEventData,
onPlace: (data, isBulk) => dispatch((isBulk ? bulkAddBoostEvent : addBoostEvent)({ query: data, data: data, tracks, areLasersLocked })),
onSelect: (data) => dispatch(selectEvent({ query: data, tracks, areLasersLocked })),
onDeselect: (data) => dispatch(deselectEvent({ query: data, tracks, areLasersLocked })),
onPick: () => {},
onDelete: (data, isBulk) => dispatch((isBulk ? bulkRemoveEvent : removeEvent)({ query: data, tracks, areLasersLocked })),
onWheel: (data, delta) => {
return dispatch(updateBoostEvent({ query: data, tracks, areLasersLocked, changes: { toggle: delta > 0 } }));
},
};
}, [dispatch, resolveEventData, tracks, areLasersLocked]);

return (
<EventGrid.Track {...api.getTrackProps(trackId, actions)} {...rest}>
<For each={boostEvents.filter((x) => x.time >= startBeat && x.time < endBeat)}>{(data) => <BoostEvent data={data} actions={actions} />}</For>
</EventGrid.Track>
);
}

export default BoostEventTrack;
20 changes: 12 additions & 8 deletions src/components/app/templates/events/controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { type ComponentProps, type CSSProperties, useMemo } from "react";
import { EventEffectIcon } from "$/components/icons";
import { Button, Field, Toggle, ToggleGroup, Tooltip } from "$/components/ui/compositions";
import { ZOOM_LEVEL_MAX, ZOOM_LEVEL_MIN } from "$/constants";
import { type ColorResolverOptions, resolveColorForItem } from "$/helpers/colors.helpers";
import type { ColorResolverOptions } from "$/helpers/colors.helpers";
import { decrementEventsEditorZoom, incrementEventsEditorZoom, updateEventsEditorColor, updateEventsEditorEditMode, updateEventsEditorMirrorLock, updateEventsEditorTool, updateEventsEditorWindowLock } from "$/store/actions";
import { useAppDispatch, useAppSelector } from "$/store/hooks";
import { selectColorScheme, selectEventsEditorColor, selectEventsEditorEditMode, selectEventsEditorMirrorLock, selectEventsEditorTool, selectEventsEditorWindowLock, selectEventsEditorZoomLevel } from "$/store/selectors";
import { EventColor, EventEditMode, EventTool } from "$/types";
import { HStack, styled } from "$:styled-system/jsx";
import { hstack } from "$:styled-system/patterns";
import { resolveColorForLightState } from "./track.helpers";

const EDIT_MODE_LIST_COLLECTION = createListCollection({
items: Object.values(EventEditMode).map((value, index) => {
Expand All @@ -21,20 +22,23 @@ const EDIT_MODE_LIST_COLLECTION = createListCollection({
}),
});

interface EventListCollection extends ColorResolverOptions {
selectedColor?: EventColor;
}
function createEventColorListCollection({ colorScheme }: EventListCollection) {
function createEventColorListCollection({ colorScheme }: ColorResolverOptions) {
return createListCollection({
items: Object.values(EventColor).map((value) => {
return { value, label: <Box style={{ "--color": resolveColorForItem(value, { colorScheme }) } as CSSProperties} /> };
const color = resolveColorForLightState({ color: value, isBoosted: false }, { colorScheme });
const boostColor = resolveColorForLightState({ color: value, isBoosted: true }, { colorScheme });
if (!color || !boostColor) return null;
return { value, label: <Box style={{ background: `linear-gradient(135deg, ${color}, ${boostColor})` } as CSSProperties} /> };
}),
});
}
function createEventEffectListCollection({ selectedColor, colorScheme }: EventListCollection) {
function createEventEffectListCollection({ selectedColor, colorScheme }: ColorResolverOptions & { selectedColor: EventColor | null }) {
return createListCollection({
items: Object.values(EventTool).map((value) => {
return { value, label: <EventEffectIcon tool={value} color={resolveColorForItem(selectedColor, { colorScheme })} /> };
const color = resolveColorForLightState({ color: selectedColor, isBoosted: false }, { colorScheme });
const boostColor = resolveColorForLightState({ color: selectedColor, isBoosted: true }, { colorScheme });
if (!color || !boostColor) return null;
return { value, label: <EventEffectIcon tool={value} color={color} boostColor={boostColor} /> };
}),
});
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/app/templates/events/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { type IEventTrack, type IEventTracks, TrackType } from "$/types";
import { clamp } from "$/utils";
import { Stack, Wrap } from "$:styled-system/jsx";
import BasicEventTrack from "./basic-track";
import BoostEventTrack from "./boost-track";

function EventGridEditor({ ...rest }: ComponentProps<typeof EventGrid.Root>) {
const { sid, bid } = useParams({ from: "/_/edit/$sid/$bid/_" });
Expand Down Expand Up @@ -168,11 +169,13 @@ function EventGridEditor({ ...rest }: ComponentProps<typeof EventGrid.Root>) {
</EventGrid.Prefix>
)}
</EventGrid.ForTracks>
<EventGrid.Prefix {...api.getPrefixProps()}>Color Boost</EventGrid.Prefix>
</EventGrid.PrefixGroup>
<EventGrid.Content {...api.getContentProps()} editMode={selectedEditMode}>
<EventGrid.Markers />
<EventGrid.Trigger ref={selectionBoxRef} {...api.getTriggerProps()}>
<EventGrid.ForTracks tracks={filteredTracks}>{(_, id) => <BasicEventTrack key={id} trackId={id} data-highlighted={isTrackDisabled(id)} />}</EventGrid.ForTracks>
<BoostEventTrack trackId={5} />
</EventGrid.Trigger>
<EventGrid.SelectionBox {...api.getSelectionBoxProps()} />
<EventGrid.Cursor {...api.getCursorProps(cursorPositionInBeats)} />
Expand Down
Loading
Loading