educational tooltips

This commit is contained in:
2026-02-01 09:26:02 +00:00
parent 8591a708e2
commit 6f9298d4bd
4 changed files with 194 additions and 12 deletions

View File

@@ -3,9 +3,11 @@ import ColorKeyboard from "@/components/controls/ColorKeyboard";
import OctaveSelector from "@/components/controls/OctaveSelector";
import PresetSelector from "@/components/controls/PresetSelector";
import { Slider } from "@/components/ui/slider";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { colorScale } from "@/constants/colorScale";
import { useShapeState } from "@/hooks/useShapeState";
import { type PreviewVoice, playPreviewSample, startPreviewVoice, stopPreviewVoice } from "@/lib/audio/synth";
import { Info } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import Layout from "./Layout";
import { cn } from "./lib/utils";
@@ -204,11 +206,37 @@ function Index() {
const sidebarContent = (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Shape</span>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Shape</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Selects the oscillator waveform. Square = pulse with odd harmonics, Circle =
sine (pure tone), Triangle = sawtooth (all harmonics).
</p>
</TooltipContent>
</Tooltip>
</div>
<PresetSelector value={state.preset} onChange={(preset) => setState({ ...state, preset })} />
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Note/Colour</span>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Note/Colour</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Sets the fundamental frequency (f) from the 12-tone chromatic scale using
equal temperament (A4 = 440Hz).
</p>
</TooltipContent>
</Tooltip>
</div>
<ColorKeyboard
value={state.color}
onChange={(color) => {
@@ -231,11 +259,37 @@ function Index() {
/>
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Octave</span>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Octave</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Transposes frequency by octaves: f = f × 2. Doubles frequency per octave,
shifting the entire harmonic series.
</p>
</TooltipContent>
</Tooltip>
</div>
<OctaveSelector value={state.octave} onChange={(octave) => setState({ ...state, octave })} />
</div>
<div className="flex flex-col gap-2">
<span className="text-sm font-medium">Size</span>
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Size</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Controls gain/amplitude in dB. Affects signal level, RMS power, and loudness
perception.
</p>
</TooltipContent>
</Tooltip>
</div>
<Slider
value={[state.size]}
min={0}
@@ -244,14 +298,32 @@ function Index() {
/>
</div>
<div className="flex flex-col gap-2">
<span
className={cn(
"text-sm font-medium",
state.preset === "circle" ? "opacity-50 pointer-events-none select-none" : "",
)}
>
Roundness
</span>
<div className="flex items-center gap-1">
<span
className={cn(
"text-sm font-medium",
state.preset === "circle" ? "opacity-50 pointer-events-none select-none" : "",
)}
>
Roundness
</span>
<Tooltip>
<TooltipTrigger asChild>
<Info
className={cn(
"size-3.5 text-muted-foreground cursor-help",
state.preset === "circle" ? "opacity-50" : "",
)}
/>
</TooltipTrigger>
<TooltipContent>
<p>
Adjusts duty cycle ratio or wave crest factor, altering the balance between
fundamental and harmonics.
</p>
</TooltipContent>
</Tooltip>
</div>
<Slider
value={[state.roundness]}
min={0}
@@ -263,6 +335,17 @@ function Index() {
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Wobble</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
LFO modulation depth in cents (±100 = ±1 semitone). Controls pitch deviation
magnitude from FM synthesis.
</p>
</TooltipContent>
</Tooltip>
<span className="text-xs text-muted-foreground">(currently only visual)</span>
</div>
<Slider
@@ -275,6 +358,17 @@ function Index() {
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Wobble Speed</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
LFO frequency in Hz. Determines how many pitch modulation cycles occur per
second.
</p>
</TooltipContent>
</Tooltip>
<span className="text-xs text-muted-foreground">(currently only visual)</span>
</div>
<Slider
@@ -287,6 +381,17 @@ function Index() {
<div className="flex flex-col gap-2">
<div className="flex items-center gap-1">
<span className="text-sm font-medium">Wobble Randomness</span>
<Tooltip>
<TooltipTrigger asChild>
<Info className="size-3.5 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
Adds noise-based jitter to the LFO signal. Breaks periodicity for organic,
non-mechanical modulation.
</p>
</TooltipContent>
</Tooltip>
<span className="text-xs text-muted-foreground">(currently only visual)</span>
</div>
<Slider

View File

@@ -0,0 +1,53 @@
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import type * as React from "react";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
);
}
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
side = "right",
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
side={side}
sideOffset={sideOffset}
className={cn(
"bg-background text-foreground border shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-auto max-w-[50vw] origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-wrap",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-background fill-background border z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };