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
8 changes: 8 additions & 0 deletions web/src/app/pattern-lab/panels/TextPanel.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
height: 32px;
}

.field input[type="range"] {
width: 100%;
min-height: 32px;
margin: 0;
accent-color: #171512;
}

.fontFile,
.actions {
display: flex;
Expand Down Expand Up @@ -136,6 +143,7 @@
@media (hover: none) {
.field select,
.field input[type="number"],
.field input[type="range"],
.fontFile button,
.actions button {
min-height: 44px;
Expand Down
42 changes: 41 additions & 1 deletion web/src/app/pattern-lab/panels/TextPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ vi.mock("@/lib/lab/textRaster", () => ({
}));

import TextPanel from "./TextPanel";
import KnobsPanel from "./KnobsPanel";
import PreviewPanel from "./PreviewPanel";
import RampPanel from "./RampPanel";
import { useLabStore } from "@/lib/lab/store";
Expand Down Expand Up @@ -160,10 +161,49 @@ describe("an effect on the text", () => {
fireEvent.change(screen.getByRole("combobox", { name: "Speed knob" }), { target: { value: "0" } });
expect(text().spec.effectKnob).toBe(0);
expect(screen.getByText(`${store().knobLabels[0]} sets how fast each LED dims`, { exact: false })).toHaveTextContent(
"It is all the way down, so nothing moves yet: turn it up in Knobs.",
"It is all the way down, so nothing moves yet: turn it up here or in Knobs.",
);
});

it("turns the speed knob beside its picker, the same knob Knobs turns", () => {
store().addTextLayer();
store().setTextSpec(text().id, { effect: "scroll" });
useLabStore.setState({ knobLabels: ["Hue", "Rate", "Mode", "Shift"] });
const [low, high] = store().ranges[1];
const textPanel = within(render(<TextPanel />).container);
const knobsPanel = within(render(<KnobsPanel />).container);
const here = textPanel.getByRole("slider", { name: "Rate" });
const there = knobsPanel.getByRole("slider", { name: "Rate value" });

fireEvent.change(here, { target: { value: String(high) } });
expect(store().knobs[1]).toBe(high);
expect(there).toHaveValue(String(high));
expect(textPanel.getByText(/Rate sets how fast/)).toHaveTextContent("It is at 100% now.");

fireEvent.change(there, { target: { value: String(low) } });
expect(here).toHaveValue(String(low));
expect(textPanel.getByText(/Rate sets how fast/)).toHaveTextContent("turn it up here or in Knobs.");
});

it("moves the slider to the knob a new pick names, and has none without an effect", () => {
store().addTextLayer();
store().setTextSpec(text().id, { effect: "twinkle" });
useLabStore.setState({ knobLabels: ["Hue", "Rate", "Mode", "Shift"] });
const { ranges } = store();
useLabStore.setState({ knobs: [ranges[0][1], ranges[1][0], ranges[2][0], ranges[3][0]] });
render(<TextPanel />);
expect(screen.getByRole("slider", { name: "Rate" })).toHaveValue(String(ranges[1][0]));
fireEvent.change(screen.getByRole("combobox", { name: "Speed knob" }), { target: { value: "0" } });
expect(screen.queryByRole("slider", { name: "Rate" })).toBeNull();
const hue = screen.getByRole("slider", { name: "Hue" });
expect(hue).toHaveValue(String(ranges[0][1]));
fireEvent.change(hue, { target: { value: String(ranges[0][0]) } });
expect(store().knobs).toEqual([ranges[0][0], ranges[1][0], ranges[2][0], ranges[3][0]]);

fireEvent.change(screen.getByRole("combobox", { name: "Effect" }), { target: { value: "none" } });
expect(screen.queryByRole("slider")).toBeNull();
});

it("names the knobs as the Knobs panel does", () => {
store().addTextLayer();
store().setTextSpec(text().id, { effect: "scroll" });
Expand Down
28 changes: 23 additions & 5 deletions web/src/app/pattern-lab/panels/TextPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
//
// An effect moves the letters every frame at a speed one knob sets
// (lib/lab/textEffects.ts), and the export and the firmware header carry it.
// That knob turns here too, beside its picker: the same store value the Knobs
// panel turns, so on a phone, where the two are tabs of one group, setting a
// speed needs no trip to Knobs and back.

import { useEffect, useRef, useState, type ChangeEvent } from "react";
import { useResettingState } from "@/lib/hooks/useResettingState";
Expand Down Expand Up @@ -170,10 +173,11 @@ export default function TextPanel() {

function TextEditor({ layer }: { layer: TextLayer }) {
const setTextSpec = useLabStore((state) => state.setTextSpec);
const setKnob = useLabStore((state) => state.setKnob);
const knobLabels = useLabStore((state) => state.knobLabels);
const speed = useLabStore((state) =>
Math.round(knobShare(state.knobs[layer.spec.effectKnob], state.ranges[layer.spec.effectKnob]) * 100),
);
const knobValue = useLabStore((state) => state.knobs[layer.spec.effectKnob] ?? 0);
const knobRange = useLabStore((state) => state.ranges[layer.spec.effectKnob]);
const speed = Math.round(knobShare(knobValue, knobRange) * 100);
const options = useFontOptions();
const fileRef = useRef<HTMLInputElement | null>(null);
const [remember, setRemember] = useState(false);
Expand Down Expand Up @@ -201,6 +205,7 @@ function TextEditor({ layer }: { layer: TextLayer }) {
options: options.filter((option) => option.group === group),
}));
const remembered = isFileFont(spec.font) && isFontRemembered(spec.font);
const knobName = knobLabels[spec.effectKnob] || `Knob ${spec.effectKnob + 1}`;

const onFile = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
Expand Down Expand Up @@ -398,12 +403,25 @@ function TextEditor({ layer }: { layer: TextLayer }) {
</select>
</label>
)}
{spec.effect !== "none" && knobRange && (
<label className={`${local.field} ${local.grow}`} title="The same knob as in Knobs: turning one turns the other">
<span>{knobName}</span>
<input
type="range"
min={knobRange[0]}
max={knobRange[1]}
step="0.001"
value={knobValue}
onChange={(event) => setKnob(spec.effectKnob, Number(event.target.value))}
/>
</label>
)}
</div>
{spec.effect !== "none" && (
<p className={local.note}>
{`${knobLabels[spec.effectKnob] || `Knob ${spec.effectKnob + 1}`} ${EFFECT_SPEED[spec.effect]}. `}
{`${knobName} ${EFFECT_SPEED[spec.effect]}. `}
{speed === 0
? "It is all the way down, so nothing moves yet: turn it up in Knobs."
? "It is all the way down, so nothing moves yet: turn it up here or in Knobs."
: `It is at ${speed}% now. All the way down stands the effect still.`}
</p>
)}
Expand Down
Loading