Improve tooltip docs with Markdown styling and refined math node explanations (#3488)

This commit is contained in:
Keavon Chambers 2025-12-20 01:05:15 -08:00 committed by GitHub
parent 2c21e1a90b
commit f1e8ebefc5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 276 additions and 185 deletions

View file

@ -70,6 +70,9 @@ pub enum FrontendMessage {
SendShortcutAltClick {
shortcut: Option<ActionShortcut>,
},
SendShortcutShiftClick {
shortcut: Option<ActionShortcut>,
},
// Trigger prefix: cause a frontend specific API to do something
TriggerAboutGraphiteLocalizedCommitDate {

View file

@ -274,7 +274,7 @@ fn static_nodes() -> Vec<DocumentNodeDefinition> {
..Default::default()
},
},
description: Cow::Borrowed("Merges new content as an entry into the graphic table that represents a layer compositing stack."),
description: Cow::Borrowed("Merges the provided content as a new element in the graphic table that represents a layer compositing stack."),
properties: None,
},
DocumentNodeDefinition {

View file

@ -124,6 +124,9 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(FrontendMessage::SendShortcutAltClick {
shortcut: action_shortcut_manual!(Key::Alt, Key::MouseLeft),
});
responses.add(FrontendMessage::SendShortcutShiftClick {
shortcut: action_shortcut_manual!(Key::Shift, Key::MouseLeft),
});
// Before loading any documents, initially prepare the welcome screen buttons layout
responses.add(PortfolioMessage::RequestWelcomeScreenButtonsLayout);

View file

@ -169,6 +169,7 @@ pub struct PivotGizmoState {
impl PivotGizmoState {
pub fn is_pivot_type(&self) -> bool {
// A disabled pivot is considered a pivot-type gizmo that is always centered
self.gizmo_type == PivotGizmoType::Pivot || self.disabled
}

View file

@ -27,7 +27,7 @@
// State provider systems
let dialog = createDialogState(editor);
setContext("dialog", dialog);
let tooltip = createTooltipState();
let tooltip = createTooltipState(editor);
setContext("tooltip", tooltip);
let document = createDocumentState(editor);
setContext("document", document);

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { onDestroy, createEventDispatcher } from "svelte";
import { getContext, onDestroy, createEventDispatcher } from "svelte";
import type { HSV, RGB, FillChoice } from "@graphite/messages";
import type { MenuDirection } from "@graphite/messages";
import type { HSV, RGB, FillChoice, MenuDirection } from "@graphite/messages";
import { Color, contrastingOutlineFactor, Gradient } from "@graphite/messages";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import { clamp } from "@graphite/utility-functions/math";
import FloatingMenu from "@graphite/components/layout/FloatingMenu.svelte";
@ -40,6 +40,7 @@
];
const dispatch = createEventDispatcher<{ colorOrGradient: FillChoice; startHistoryTransaction: undefined }>();
const tooltip = getContext<TooltipState>("tooltip");
export let colorOrGradient: FillChoice;
export let allowNone = false;
@ -424,12 +425,16 @@
"--opaque-color-contrasting": (newColor.opaque() || new Color(0, 0, 0, 1)).contrastingColor(),
}}
>
{@const hueDescription = "The shade along the spectrum of the rainbow."}
{@const saturationDescription = "The vividness from grayscale to full color."}
{@const valueDescription = "The brightness from black to full color."}
<LayoutCol class="pickers-and-gradient">
<LayoutRow class="pickers">
<LayoutCol
class="saturation-value-picker"
data-tooltip-label="Saturation and Value"
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
data-tooltip-description={`To move only along the saturation (X) or value (Y) axis, perform the shortcut shown.${disabled ? "\n\nDisabled (read-only)." : ""}`}
data-tooltip-shortcut={$tooltip.shiftClickShortcut?.shortcut ? JSON.stringify($tooltip.shiftClickShortcut.shortcut) : undefined}
on:pointerdown={onPointerDown}
data-saturation-value-picker
>
@ -449,7 +454,7 @@
<LayoutCol
class="hue-picker"
data-tooltip-label="Hue"
data-tooltip-description={`The shade along the spectrum of the rainbow.${disabled ? "\n\nDisabled (read-only)." : ""}`}
data-tooltip-description={`${hueDescription}${disabled ? "\n\nDisabled (read-only)." : ""}`}
on:pointerdown={onPointerDown}
data-hue-picker
>
@ -522,10 +527,8 @@
</LayoutRow>
<!-- <DropdownInput entries={[[{ label: "sRGB" }]]} selectedIndex={0} disabled={true} tooltipDescription="Color model, color space, and HDR (coming soon)." /> -->
<LayoutRow>
<TextLabel
tooltipLabel="Hex Color Code"
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors.">Hex</TextLabel
>
{@const hexDescription = "Color code in hexadecimal format. 6 digits if opaque, 8 with alpha. Accepts input of CSS color values including named colors."}
<TextLabel tooltipLabel="Hex Color Code" tooltipDescription={hexDescription}>Hex</TextLabel>
<Separator type="Related" />
<LayoutRow>
<TextInput
@ -537,7 +540,7 @@
}}
centered={true}
tooltipLabel="Hex Color Code"
tooltipDescription="Color code in hexadecimal format. 6 digits if opaque, 8 with alpha.\nAccepts input of CSS color values including named colors."
tooltipDescription={hexDescription}
bind:this={hexCodeInputWidget}
/>
</LayoutRow>
@ -601,16 +604,17 @@
v: "Value Component",
}[channel]}
tooltipDescription={{
h: "The shade along the spectrum of the rainbow.",
s: "The vividness from grayscale to full color.",
v: "The brightness from black to full color.",
h: hueDescription,
s: saturationDescription,
v: valueDescription,
}[channel]}
/>
{/each}
</LayoutRow>
</LayoutRow>
<LayoutRow>
<TextLabel tooltipLabel="Alpha" tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%).">Alpha</TextLabel>
{@const alphaDescription = "The level of translucency, from transparent (0%) to opaque (100%)."}
<TextLabel tooltipLabel="Alpha" tooltipDescription={alphaDescription}>Alpha</TextLabel>
<Separator type="Related" />
<NumberInput
value={!isNone ? alpha * 100 : undefined}
@ -630,7 +634,7 @@
mode="Range"
displayDecimalPlaces={1}
tooltipLabel="Alpha"
tooltipDescription="The level of translucency, from transparent (0%) to opaque (100%)."
tooltipDescription={alphaDescription}
/>
</LayoutRow>
<LayoutRow class="leftover-space" />
@ -670,7 +674,7 @@
data-pure-tile={name.toLowerCase()}
style:--pure-color={color}
style:--pure-color-gray={gray}
data-tooltip-label="Set to Red"
data-tooltip-label={`Set to ${name}`}
data-tooltip-description={disabled ? "Disabled (read-only)." : ""}
/>
{/each}

View file

@ -15,8 +15,8 @@
let self: FloatingMenu | undefined;
$: label = filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim());
$: description = filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim());
$: label = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-label")?.trim()));
$: description = parseMarkdown(filterTodo($tooltip.element?.getAttribute("data-tooltip-description")?.trim()));
$: shortcutJSON = $tooltip.element?.getAttribute("data-tooltip-shortcut")?.trim();
$: shortcut = ((shortcutJSON) => {
if (!shortcutJSON) return undefined;
@ -32,6 +32,26 @@
if (text?.trim().toUpperCase() === "TODO" && !editor.handle.inDevelopmentMode()) return "";
return text;
}
function parseMarkdown(markdown: string | undefined): string | undefined {
if (!markdown) return undefined;
return (
markdown
// .split("\n")
// .map((line) => line.trim())
// .join("\n")
// .split("\n\n")
// .map((paragraph) => paragraph.replaceAll("\n", " "))
// .join("\n\n")
// Bold
.replace(/\*\*((?:(?!\*\*).)+)\*\*/g, "<strong>$1</strong>")
// Italic
.replace(/\*([^*]+)\*/g, "<em>$1</em>")
// Backticks
.replace(/`([^`]+)`/g, "<code>$1</code>")
);
}
</script>
{#if label || description}
@ -40,7 +60,7 @@
{#if label || shortcut}
<LayoutRow class="tooltip-header">
{#if label}
<TextLabel class="tooltip-label">{label}</TextLabel>
<TextLabel class="tooltip-label">{@html label}</TextLabel>
{/if}
{#if shortcut}
<ShortcutLabel shortcut={{ shortcut }} />
@ -48,7 +68,7 @@
</LayoutRow>
{/if}
{#if description}
<TextLabel class="tooltip-description">{description}</TextLabel>
<TextLabel class="tooltip-description">{@html description}</TextLabel>
{/if}
</FloatingMenu>
</div>

View file

@ -9,10 +9,10 @@
UpdateLayersPanelControlBarLeftLayout,
UpdateLayersPanelControlBarRightLayout,
UpdateLayersPanelBottomBarLayout,
SendShortcutAltClick,
} from "@graphite/messages";
import type { ActionShortcut, DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
import type { DataBuffer, LayerPanelEntry, Layout } from "@graphite/messages";
import type { NodeGraphState } from "@graphite/state-providers/node-graph";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import { operatingSystem } from "@graphite/utility-functions/platform";
import { extractPixelData } from "@graphite/utility-functions/rasterization";
@ -49,6 +49,7 @@
const editor = getContext<Editor>("editor");
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
const tooltip = getContext<TooltipState>("tooltip");
let list: LayoutCol | undefined;
@ -73,13 +74,7 @@
let layersPanelControlBarRightLayout: Layout = [];
let layersPanelBottomBarLayout: Layout = [];
let altClickShortcut: ActionShortcut | undefined;
onMount(() => {
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
altClickShortcut = data.shortcut;
});
editor.subscriptions.subscribeJsMessage(UpdateLayersPanelControlBarLeftLayout, (updateLayersPanelControlBarLeftLayout) => {
patchLayout(layersPanelControlBarLeftLayout, updateLayersPanelControlBarLeftLayout);
layersPanelControlBarLeftLayout = layersPanelControlBarLeftLayout;
@ -628,7 +623,7 @@
? "Hide the layers nested within. (To affect all open descendants, perform the shortcut shown.)"
: "Show the layers nested within. (To affect all closed descendants, perform the shortcut shown.)") +
(listing.entry.ancestorOfSelected && !listing.entry.expanded ? "\n\nA selected layer is currently contained within.\n" : "")}
data-tooltip-shortcut={altClickShortcut?.shortcut ? JSON.stringify(altClickShortcut.shortcut) : undefined}
data-tooltip-shortcut={$tooltip.altClickShortcut?.shortcut ? JSON.stringify($tooltip.altClickShortcut.shortcut) : undefined}
on:click={(e) => handleExpandArrowClickWithModifiers(e, listing.entry.id)}
tabindex="0"
></button>
@ -639,8 +634,9 @@
<IconLabel
icon="Clipped"
class="clipped-arrow"
tooltipDescription="Clipping mask is active. To release it, perform the shortcut on the layer border."
tooltipShortcut={altClickShortcut}
tooltipLabel="Layer Clipped"
tooltipDescription="Clipping mask is active. To release it, target the bottom border of the layer and perform the shortcut shown."
tooltipShortcut={$tooltip.altClickShortcut}
/>
{/if}
<div class="thumbnail">

View file

@ -501,7 +501,7 @@
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
style:--layer-area-width={layerAreaWidth}
style:--node-chain-area-left-extension={layerChainWidth !== 0 ? layerChainWidth + 0.5 : 0}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()}
@ -651,7 +651,7 @@
style:--clip-path-id={`url(#${clipPathId})`}
style:--data-color={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()})`}
style:--data-color-dim={`var(--color-data-${(node.primaryOutput?.dataType || "General").toLowerCase()}-dim)`}
data-tooltip-label={node.displayName === node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-label={node.displayName === node.reference || !node.reference ? node.displayName : `${node.displayName} (${node.reference})`}
data-tooltip-description={`
${(description || "").trim()}${editor.handle.inDevelopmentMode() ? `\n\nID: ${node.id}. Position: (${node.position.x}, ${node.position.y}).` : ""}
`.trim()}

View file

@ -72,7 +72,8 @@
font-style: italic;
}
&.monospace {
&.monospace,
code {
font-family: "Source Code Pro", monospace;
font-size: 12px;
}
@ -94,5 +95,10 @@
a {
color: inherit;
}
code {
background: var(--color-3-darkgray);
padding: 0 2px;
}
}
</style>

View file

@ -1,24 +1,15 @@
<script lang="ts">
import { getContext, onMount } from "svelte";
import { getContext } from "svelte";
import type { Editor } from "@graphite/editor";
import type { ActionShortcut } from "@graphite/messages";
import { SendShortcutF11 } from "@graphite/messages";
import type { FullscreenState } from "@graphite/state-providers/fullscreen";
import type { TooltipState } from "@graphite/state-providers/tooltip";
import LayoutRow from "@graphite/components/layout/LayoutRow.svelte";
import IconLabel from "@graphite/components/widgets/labels/IconLabel.svelte";
const fullscreen = getContext<FullscreenState>("fullscreen");
const editor = getContext<Editor>("editor");
let f11Shortcut: ActionShortcut | undefined = undefined;
onMount(() => {
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
f11Shortcut = data.shortcut;
});
});
const tooltip = getContext<TooltipState>("tooltip");
async function handleClick() {
if ($fullscreen.windowFullscreen) fullscreen.exitFullscreen();
@ -31,7 +22,7 @@
on:click={handleClick}
tooltipLabel={$fullscreen.windowFullscreen ? "Exit Fullscreen" : "Enter Fullscreen"}
tooltipDescription={$fullscreen.keyboardLockApiSupported ? "While fullscreen, keyboard shortcuts normally reserved by the browser become available." : ""}
tooltipShortcut={f11Shortcut}
tooltipShortcut={$tooltip.f11Shortcut}
>
<IconLabel icon={$fullscreen.windowFullscreen ? "FullscreenExit" : "FullscreenEnter"} />
</LayoutRow>

View file

@ -481,11 +481,11 @@ export function createInputManager(editor: Editor, dialog: DialogState, portfoli
The browser's clipboard permission has been denied.
Open the browser's website settings (usually accessible
just left of the URL) to allow this permission.
just left of the URL bar) to allow this permission.
`;
const nothing = stripIndents`
No valid clipboard data was found. You may have better
luck pasting with the standard keyboard shortcut instead.
success pasting with the standard keyboard shortcut instead.
`;
const matchMessage = {

View file

@ -119,6 +119,11 @@ export class SendShortcutAltClick extends JsMessage {
readonly shortcut!: ActionShortcut | undefined;
}
export class SendShortcutShiftClick extends JsMessage {
@Transform(({ value }: { value: ActionShortcut }) => value || undefined)
readonly shortcut!: ActionShortcut | undefined;
}
export class UpdateNodeThumbnail extends JsMessage {
readonly id!: bigint;
@ -1696,6 +1701,7 @@ export const messageMakers: Record<string, MessageMaker> = {
SendUIMetadata,
SendShortcutF11,
SendShortcutAltClick,
SendShortcutShiftClick,
TriggerAboutGraphiteLocalizedCommitDate,
TriggerDisplayThirdPartyLicensesDialog,
TriggerExportImage,

View file

@ -9,7 +9,7 @@ export function createAppWindowState(editor: Editor) {
maximized: false,
fullscreen: false,
viewportHolePunch: false,
uiScale: 1.0,
uiScale: 1,
});
// Set up message subscriptions on creation

View file

@ -1,12 +1,18 @@
import { writable } from "svelte/store";
import { type Editor } from "@graphite/editor";
import { SendShortcutAltClick, SendShortcutF11, SendShortcutShiftClick, type ActionShortcut } from "@graphite/messages";
const SHOW_TOOLTIP_DELAY_MS = 500;
export function createTooltipState() {
export function createTooltipState(editor: Editor) {
const { subscribe, update } = writable({
visible: false,
element: undefined as Element | undefined,
position: { x: 0, y: 0 },
shiftClickShortcut: undefined as ActionShortcut | undefined,
altClickShortcut: undefined as ActionShortcut | undefined,
f11Shortcut: undefined as ActionShortcut | undefined,
});
let tooltipTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
@ -45,6 +51,25 @@ export function createTooltipState() {
}, SHOW_TOOLTIP_DELAY_MS);
});
editor.subscriptions.subscribeJsMessage(SendShortcutShiftClick, async (data) => {
update((state) => {
state.shiftClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeJsMessage(SendShortcutAltClick, async (data) => {
update((state) => {
state.altClickShortcut = data.shortcut;
return state;
});
});
editor.subscriptions.subscribeJsMessage(SendShortcutF11, async (data) => {
update((state) => {
state.f11Shortcut = data.shortcut;
return state;
});
});
document.addEventListener("mousedown", closeTooltip);
document.addEventListener("keydown", closeTooltip);

View file

@ -7,7 +7,7 @@ use glam::DVec2;
use kurbo::{BezPath, CubicBez, Line, ParamCurve, PathSeg, Point, QuadBez};
use std::ops::Sub;
/// Represents different ways of calculating the centroid.
/// Represents different geometric interpretations of calculating the centroid (center of mass).
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]
pub enum CentroidType {

View file

@ -194,7 +194,6 @@ impl From<Fill> for FillChoice {
}
}
/// Enum describing the type of [Fill].
#[repr(C)]
#[derive(Debug, Clone, Copy, Default, PartialEq, serde::Serialize, serde::Deserialize, DynAny, Hash, specta::Type, node_macro::ChoiceType)]
#[widget(Radio)]

View file

@ -38,7 +38,7 @@ fn math<T: num_traits::float::Float>(
/// The value of "A" when calculating the expression.
#[implementations(f64, f32)]
operand_a: T,
/// A math expression that may incorporate "A" and/or "B", such as "sqrt(A + B) - B^2".
/// A math expression that may incorporate "A" and/or "B", such as `sqrt(A + B) - B^2`.
#[default(A + B)]
expression: String,
/// The value of "B" when calculating the expression.
@ -76,98 +76,100 @@ fn math<T: num_traits::float::Float>(
}
}
/// The addition operation (+) calculates the sum of two numbers.
/// The addition operation (`+`) calculates the sum of two scalar numbers or vectors.
#[node_macro::node(category("Math: Arithmetic"))]
fn add<U: Add<T>, T>(
fn add<A: Add<B>, B>(
_: impl Ctx,
/// The left-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
augend: U,
augend: A,
/// The right-hand side of the addition operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
addend: T,
) -> <U as Add<T>>::Output {
addend: B,
) -> <A as Add<B>>::Output {
augend + addend
}
/// The subtraction operation (-) calculates the difference between two numbers.
/// The subtraction operation (`-`) calculates the difference between two scalar numbers or vectors.
#[node_macro::node(category("Math: Arithmetic"))]
fn subtract<U: Sub<T>, T>(
fn subtract<A: Sub<B>, B>(
_: impl Ctx,
/// The left-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
minuend: U,
minuend: A,
/// The right-hand side of the subtraction operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
subtrahend: T,
) -> <U as Sub<T>>::Output {
subtrahend: B,
) -> <A as Sub<B>>::Output {
minuend - subtrahend
}
/// The multiplication operation (×) calculates the product of two numbers.
/// The multiplication operation (`×`) calculates the product of two scalar numbers, vectors, or transforms.
#[node_macro::node(category("Math: Arithmetic"))]
fn multiply<U: Mul<T>, T>(
fn multiply<A: Mul<B>, B>(
_: impl Ctx,
/// The left-hand side of the multiplication operation.
#[implementations(f64, f32, u32, DVec2, f64, DVec2, DAffine2)]
multiplier: U,
multiplier: A,
/// The right-hand side of the multiplication operation.
#[default(1.)]
#[implementations(f64, f32, u32, DVec2, DVec2, f64, DAffine2)]
multiplicand: T,
) -> <U as Mul<T>>::Output {
multiplicand: B,
) -> <A as Mul<B>>::Output {
multiplier * multiplicand
}
/// The division operation (÷) calculates the quotient of two numbers.
/// The division operation (`÷`) calculates the quotient of two scalar numbers or vectors.
///
/// Produces 0 if the denominator is 0.
#[node_macro::node(category("Math: Arithmetic"))]
fn divide<U: Div<T> + Default + PartialEq, T: Default + PartialEq>(
fn divide<A: Div<B> + Default + PartialEq, B: Default + PartialEq>(
_: impl Ctx,
/// The left-hand side of the division operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U,
numerator: A,
/// The right-hand side of the division operation.
#[default(1.)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
denominator: T,
) -> <U as Div<T>>::Output
denominator: B,
) -> <A as Div<B>>::Output
where
<U as Div<T>>::Output: Default,
<A as Div<B>>::Output: Default,
{
if denominator == T::default() {
return <U as Div<T>>::Output::default();
if denominator == B::default() {
return <A as Div<B>>::Output::default();
}
numerator / denominator
}
/// The modulo operation (%) calculates the remainder from the division of two numbers. The sign of the result shares the sign of the numerator unless "Always Positive" is enabled.
/// The modulo operation (`%`) calculates the remainder from the division of two scalar numbers or vectors.
///
/// The sign of the result shares the sign of the numerator unless *Always Positive* is enabled.
#[node_macro::node(category("Math: Arithmetic"))]
fn modulo<U: Rem<T, Output: Add<T, Output: Rem<T, Output = U::Output>>>, T: Copy>(
fn modulo<A: Rem<B, Output: Add<B, Output: Rem<B, Output = A::Output>>>, B: Copy>(
_: impl Ctx,
/// The left-hand side of the modulo operation.
#[implementations(f64, f32, u32, DVec2, DVec2, f64)]
numerator: U,
numerator: A,
/// The right-hand side of the modulo operation.
#[default(2.)]
#[implementations(f64, f32, u32, DVec2, f64, DVec2)]
modulus: T,
/// Ensures the result will always be positive, even if the numerator is negative.
modulus: B,
/// Ensures the result is always positive, even if the numerator is negative.
#[default(true)]
always_positive: bool,
) -> <U as Rem<T>>::Output {
) -> <A as Rem<B>>::Output {
if always_positive { (numerator % modulus + modulus) % modulus } else { numerator % modulus }
}
/// The exponent operation (^) calculates the result of raising a number to a power.
/// The exponent operation (`^`) calculates the result of raising a number to a power.
#[node_macro::node(category("Math: Arithmetic"))]
fn exponent<T: Pow<T>>(
_: impl Ctx,
/// The base number that will be raised to the power.
/// The base number that is raised to the power.
#[implementations(f64, f32, u32)]
base: T,
/// The power to which the base number will be raised.
/// The power to which the base number is raised.
#[implementations(f64, f32, u32)]
#[default(2.)]
power: T,
@ -175,15 +177,18 @@ fn exponent<T: Pow<T>>(
base.pow(power)
}
/// The square root operation (√) calculates the nth root of a number, equivalent to raising the number to the power of 1/n.
/// The `n`th root operation (`√`) calculates the inverse of exponentiation. Square root inverts squaring, cube root inverts cubing, and so on.
///
/// This is equivalent to raising the number to the power of `1/n`.
#[node_macro::node(category("Math: Arithmetic"))]
fn root<T: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the nth root will be calculated.
/// The number inside the radical for which the `n`th root is calculated.
#[default(2.)]
#[implementations(f64, f32)]
radicand: T,
/// The degree of the root to be calculated. Square root is 2, cube root is 3, and so on.
/// Degrees 0 or less are invalid and will produce an output of 0.
#[default(2.)]
#[implementations(f64, f32)]
degree: T,
@ -192,16 +197,18 @@ fn root<T: num_traits::float::Float>(
radicand.sqrt()
} else if degree == T::from(3.).unwrap() {
radicand.cbrt()
} else if degree <= T::from(0.).unwrap() {
T::from(0.).unwrap()
} else {
radicand.powf(T::from(1.).unwrap() / degree)
}
}
/// The logarithmic function (log) calculates the logarithm of a number with a specified base. If the natural logarithm function (ln) is desired, set the base to "e".
/// The logarithmic function (`log`) calculates the logarithm of a number with a specified base. If the natural logarithm function (`ln`) is desired, set the base to "e".
#[node_macro::node(category("Math: Arithmetic"))]
fn logarithm<T: num_traits::float::Float>(
_: impl Ctx,
/// The number for which the logarithm will be calculated.
/// The number for which the logarithm is calculated.
#[implementations(f64, f32)]
value: T,
/// The base of the logarithm, such as 2 (binary), 10 (decimal), and e (natural logarithm).
@ -220,7 +227,7 @@ fn logarithm<T: num_traits::float::Float>(
}
}
/// The sine trigonometric function (sin) calculates the ratio of the angle's opposite side length to its hypotenuse length.
/// The sine trigonometric function (`sin`) calculates the ratio of the angle's opposite side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn sine<T: num_traits::float::Float>(
_: impl Ctx,
@ -233,7 +240,7 @@ fn sine<T: num_traits::float::Float>(
if radians { theta.sin() } else { theta.to_radians().sin() }
}
/// The cosine trigonometric function (cos) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
/// The cosine trigonometric function (`cos`) calculates the ratio of the angle's adjacent side length to its hypotenuse length.
#[node_macro::node(category("Math: Trig"))]
fn cosine<T: num_traits::float::Float>(
_: impl Ctx,
@ -246,7 +253,7 @@ fn cosine<T: num_traits::float::Float>(
if radians { theta.cos() } else { theta.to_radians().cos() }
}
/// The tangent trigonometric function (tan) calculates the ratio of the angle's opposite side length to its adjacent side length.
/// The tangent trigonometric function (`tan`) calculates the ratio of the angle's opposite side length to its adjacent side length.
#[node_macro::node(category("Math: Trig"))]
fn tangent<T: num_traits::float::Float>(
_: impl Ctx,
@ -259,41 +266,43 @@ fn tangent<T: num_traits::float::Float>(
if radians { theta.tan() } else { theta.to_radians().tan() }
}
/// The inverse sine trigonometric function (asin) calculates the angle whose sine is the specified value.
/// The inverse sine trigonometric function (`asin`) calculates the angle whose sine is the input value.
#[node_macro::node(category("Math: Trig"))]
fn sine_inverse<T: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
/// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
#[implementations(f64, f32)]
value: T,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> T {
if radians { value.asin() } else { value.asin().to_degrees() }
let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).asin();
if radians { angle } else { angle.to_degrees() }
}
/// The inverse cosine trigonometric function (acos) calculates the angle whose cosine is the specified value.
/// The inverse cosine trigonometric function (`acos`) calculates the angle whose cosine is the input value.
#[node_macro::node(category("Math: Trig"))]
fn cosine_inverse<T: num_traits::float::Float>(
_: impl Ctx,
/// The given value for which the angle will be calculated. Must be in the range [-1, 1] or else the result will be NaN.
/// The given value for which the angle is calculated. Must be in the domain `[-1, 1]` (it will be clamped to -1 or 1 otherwise).
#[implementations(f64, f32)]
value: T,
/// Whether the resulting angle should be given in as radians instead of degrees.
radians: bool,
) -> T {
if radians { value.acos() } else { value.acos().to_degrees() }
let angle = value.clamp(T::from(-1.).unwrap(), T::from(1.).unwrap()).acos();
if radians { angle } else { angle.to_degrees() }
}
/// The inverse tangent trigonometric function (atan or atan2, depending on input type) calculates:
/// atan: the angle whose tangent is the specified scalar number.
/// atan2: the angle of a ray from the origin to the specified vec2.
/// The inverse tangent trigonometric function (`atan` or `atan2`, depending on input type) calculates:
/// `atan`: the angle whose tangent is the input scalar number.
/// `atan2`: the angle of a ray from the origin to the input vec2.
///
/// The resulting angle is always in the range [-90°, 90°] or, in radians, [-π/2, π/2].
/// The resulting angle is always in the range `[-90°, 90°]` or, in radians, `[-π/2, π/2]`.
#[node_macro::node(category("Math: Trig"))]
fn tangent_inverse<T: TangentInverse>(
_: impl Ctx,
/// The given value for which the angle will be calculated.
/// The given value for which the angle is calculated.
#[implementations(f64, f32, DVec2)]
value: T,
/// Whether the resulting angle should be given in as radians instead of degrees.
@ -325,18 +334,30 @@ impl TangentInverse for DVec2 {
}
}
/// Linearly maps an input value from one range to another. The ranges may be reversed.
///
/// For example, 0.5 in the input range `[0, 1]` would map to 0 in the output range `[-180, 180]`.
#[node_macro::node(category("Math: Numeric"))]
fn remap<U: num_traits::float::Float>(
_: impl Ctx,
#[implementations(f64, f32)] value: U,
#[implementations(f64, f32)] input_min: U,
/// The value to be mapped between ranges.
#[implementations(f64, f32)]
value: U,
/// The lower bound of the input range.
#[implementations(f64, f32)]
input_min: U,
/// The upper bound of the input range.
#[implementations(f64, f32)]
#[default(1.)]
input_max: U,
#[implementations(f64, f32)] output_min: U,
/// The lower bound of the output range.
#[implementations(f64, f32)]
output_min: U,
/// The upper bound of the output range.
#[implementations(f64, f32)]
#[default(1.)]
output_max: U,
/// Whether to constrain the result within the output range instead of extrapolating beyond its bounds.
clamped: bool,
) -> U {
let input_range = input_max - input_min;
@ -363,17 +384,17 @@ fn remap<U: num_traits::float::Float>(
}
}
/// The random function (rand) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
/// The random function (`rand`) converts a seed into a random number within the specified range, inclusive of the minimum and exclusive of the maximum. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn random(
_: impl Ctx,
_primary: (),
/// Seed to determine the unique variation of which number will be generated.
/// Seed to determine the unique variation of which number is generated.
seed: u64,
/// The smaller end of the range within which the random number will be generated.
/// The smaller end of the range within which the random number is generated.
#[default(0.)]
min: f64,
/// The larger end of the range within which the random number will be generated.
/// The larger end of the range within which the random number is generated.
#[default(1.)]
max: f64,
) -> f64 {
@ -404,89 +425,89 @@ fn to_f64(_: impl Ctx, value: f64) -> f64 {
value
}
/// The rounding function (round) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
/// The rounding function (`round`) maps an input value to its nearest whole number. Halfway values are rounded away from zero.
#[node_macro::node(category("Math: Numeric"))]
fn round<T: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded.
/// The number to be rounded to the nearest whole number.
#[implementations(f64, f32)]
value: T,
) -> T {
value.round()
}
/// The floor function (floor) rounds down an input value to the nearest whole number, unless the input number is already whole.
/// The floor function (`floor`) rounds down an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn floor<T: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded down.
/// The number to be rounded down.
#[implementations(f64, f32)]
value: T,
) -> T {
value.floor()
}
/// The ceiling function (ceil) rounds up an input value to the nearest whole number, unless the input number is already whole.
/// The ceiling function (`ceil`) rounds up an input value to the nearest whole number, unless the input number is already whole.
#[node_macro::node(category("Math: Numeric"))]
fn ceiling<T: num_traits::float::Float>(
_: impl Ctx,
/// The number which will be rounded up.
/// The number to be rounded up.
#[implementations(f64, f32)]
value: T,
) -> T {
value.ceil()
}
/// The absolute value function (abs) removes the negative sign from an input value, if present.
/// The absolute value function (`abs`) removes the negative sign from an input value, if present.
#[node_macro::node(category("Math: Numeric"))]
fn absolute_value<T: num_traits::sign::Signed>(
_: impl Ctx,
/// The number which will be made positive.
/// The number to be made positive.
#[implementations(f64, f32, i32, i64)]
value: T,
) -> T {
value.abs()
}
/// The minimum function (min) picks the smaller of two numbers.
/// The minimum function (`min`) picks the smaller of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn min<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the lesser will be returned.
/// One of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the lesser will be returned.
/// The other of the two numbers, of which the lesser is returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value < other_value { value } else { other_value }
}
/// The maximum function (max) picks the larger of two numbers.
/// The maximum function (`max`) picks the larger of two numbers.
#[node_macro::node(category("Math: Numeric"))]
fn max<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// One of the two numbers, of which the greater will be returned.
/// One of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The other of the two numbers, of which the greater will be returned.
/// The other of the two numbers, of which the greater is returned.
#[implementations(f64, f32, u32, &str)]
other_value: T,
) -> T {
if value > other_value { value } else { other_value }
}
/// The clamp function (clamp) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
/// The clamp function (`clamp`) restricts a number to a specified range between a minimum and maximum value. The minimum and maximum values are automatically swapped if they are reversed.
#[node_macro::node(category("Math: Numeric"))]
fn clamp<T: std::cmp::PartialOrd>(
_: impl Ctx,
/// The number to be clamped, which will be restricted to the range between the minimum and maximum values.
/// The number to be clamped, which is restricted to the range between the minimum and maximum values.
#[implementations(f64, f32, u32, &str)]
value: T,
/// The left (smaller) side of the range. The output will never be less than this number.
/// The left (smaller) side of the range. The output is never less than this number.
#[implementations(f64, f32, u32, &str)]
min: T,
/// The right (greater) side of the range. The output will never be greater than this number.
/// The right (greater) side of the range. The output is never greater than this number.
#[implementations(f64, f32, u32, &str)]
max: T,
) -> T {
@ -504,10 +525,10 @@ fn clamp<T: std::cmp::PartialOrd>(
#[node_macro::node(category("Math: Numeric"))]
fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops::SubAssign>(
_: impl Ctx,
/// One of the two numbers for which the GCD will be calculated.
/// One of the two numbers for which the GCD is calculated.
#[implementations(u32, u64, i32)]
value: T,
/// The other of the two numbers for which the GCD will be calculated.
/// The other of the two numbers for which the GCD is calculated.
#[implementations(u32, u64, i32)]
other_value: T,
) -> T {
@ -524,10 +545,10 @@ fn greatest_common_divisor<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32
#[node_macro::node(category("Math: Numeric"))]
fn least_common_multiple<T: num_traits::ToPrimitive + num_traits::FromPrimitive + num_traits::identities::Zero>(
_: impl Ctx,
/// One of the two numbers for which the LCM will be calculated.
/// One of the two numbers for which the LCM is calculated.
#[implementations(u32, u64, i32)]
value: T,
/// The other of the two numbers for which the LCM will be calculated.
/// The other of the two numbers for which the LCM is calculated.
#[implementations(u32, u64, i32)]
other_value: T,
) -> T {
@ -574,36 +595,8 @@ fn binary_gcd<T: num_traits::int::PrimInt + std::ops::ShrAssign<i32> + std::ops:
a << shift
}
/// The equality operation (==) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
value: T,
/// The other of the two numbers to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
other_value: T,
) -> bool {
other_value == value
}
/// The inequality operation (!=) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
value: T,
/// The other of the two numbers to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
other_value: T,
) -> bool {
other_value != value
}
/// The less-than operation (<) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with "Or Equal", the less-than-or-equal operation (<=) will be used instead.
/// The less-than operation (`<`) compares two values and returns true if the first value is less than the second, or false if it is not.
/// If enabled with *Or Equal*, the less-than-or-equal operation (`<=`) is used instead.
#[node_macro::node(category("Math: Logic"))]
fn less_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
@ -613,14 +606,14 @@ fn less_than<T: std::cmp::PartialOrd<T>>(
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the less-than-or-equal operation (<=) instead of the less-than operation (<).
/// Uses the less-than-or-equal operation (`<=`) instead of the less-than operation (`<`).
or_equal: bool,
) -> bool {
if or_equal { value <= other_value } else { value < other_value }
}
/// The greater-than operation (>) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with "Or Equal", the greater-than-or-equal operation (>=) will be used instead.
/// The greater-than operation (`>`) compares two values and returns true if the first value is greater than the second, or false if it is not.
/// If enabled with *Or Equal*, the greater-than-or-equal operation (`>=`) is used instead.
#[node_macro::node(category("Math: Logic"))]
fn greater_than<T: std::cmp::PartialOrd<T>>(
_: impl Ctx,
@ -630,13 +623,41 @@ fn greater_than<T: std::cmp::PartialOrd<T>>(
/// The number on the right-hand side of the comparison.
#[implementations(f64, f32, u32)]
other_value: T,
/// Uses the greater-than-or-equal operation (>=) instead of the greater-than operation (>).
/// Uses the greater-than-or-equal operation (`>=`) instead of the greater-than operation (`>`).
or_equal: bool,
) -> bool {
if or_equal { value >= other_value } else { value > other_value }
}
/// The logical or operation (||) returns true if either of the two inputs are true, or false if both are false.
/// The equality operation (`==`, `XNOR`) compares two values and returns true if they are equal, or false if they are not.
#[node_macro::node(category("Math: Logic"))]
fn equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
value: T,
/// The other of the two values to compare for equality.
#[implementations(f64, f32, u32, DVec2, bool, &str, String)]
other_value: T,
) -> bool {
other_value == value
}
/// The inequality operation (`!=`, `XOR`) compares two values and returns true if they are not equal, or false if they are.
#[node_macro::node(category("Math: Logic"))]
fn not_equals<T: std::cmp::PartialEq<T>>(
_: impl Ctx,
/// One of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
value: T,
/// The other of the two values to compare for inequality.
#[implementations(f64, f32, u32, DVec2, bool, &str)]
other_value: T,
) -> bool {
other_value != value
}
/// The logical OR operation (`||`) returns true if either of the two inputs are true, or false if both are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_or(
_: impl Ctx,
@ -648,7 +669,7 @@ fn logical_or(
value || other_value
}
/// The logical and operation (&&) returns true if both of the two inputs are true, or false if any are false.
/// The logical AND operation (`&&`) returns true if both of the two inputs are true, or false if any are false.
#[node_macro::node(category("Math: Logic"))]
fn logical_and(
_: impl Ctx,
@ -660,7 +681,7 @@ fn logical_and(
value && other_value
}
/// The logical not operation (!) reverses true and false value of the input.
/// The logical NOT operation (`!`) reverses true and false value of the input.
#[node_macro::node(category("Math: Logic"))]
fn logical_not(
_: impl Ctx,
@ -736,20 +757,39 @@ fn footprint_value(_: impl Ctx, _primary: (), transform: DAffine2, #[default(100
}
}
/// The dot product operation (`·`) calculates the degree of similarity of a vec2 pair based on their angles and lengths.
///
/// Calculated as `‖a‖‖b‖cos(θ)`, it represents the product of their lengths (`‖a‖‖b‖`) scaled by the alignment of their directions (`cos(θ)`).
/// The output ranges from the positive to negative product of their lengths based on when they are pointing in the same or opposite directions.
/// If any vector has zero length, the output is 0.
#[node_macro::node(category("Math: Vector"))]
fn dot_product(_: impl Ctx, vector_a: DVec2, vector_b: DVec2) -> f64 {
vector_a.dot(vector_b)
fn dot_product(
_: impl Ctx,
/// An operand of the dot product operation.
vector_a: DVec2,
/// The other operand of the dot product operation.
#[default((1., 0.))]
vector_b: DVec2,
/// Whether to normalize both input vectors so the calculation ranges in `[-1, 1]` by considering only their degree of directional alignment.
normalize: bool,
) -> f64 {
if normalize {
vector_a.normalize_or_zero().dot(vector_b.normalize_or_zero())
} else {
vector_a.dot(vector_b)
}
}
/// Gets the length or magnitude of a vector.
// TODO: Rename to "Magnitude"
/// The magnitude operator (`‖x‖`) calculates the length of a vec2, which is the distance from the base to the tip of the arrow represented by the vector.
#[node_macro::node(category("Math: Vector"))]
fn length(_: impl Ctx, vector: DVec2) -> f64 {
vector.length()
}
/// Scales the input vector to unit length while preserving it's direction. This is equivalent to dividing the input vector by it's own magnitude.
/// Scales the input vector to unit length while preserving its direction. This is equivalent to dividing the input vector by its own magnitude.
///
/// Returns zero when the input vector is zero.
/// Returns 0 when the input vector has zero length.
#[node_macro::node(category("Math: Vector"))]
fn normalize(_: impl Ctx, vector: DVec2) -> DVec2 {
vector.normalize_or_zero()
@ -765,7 +805,7 @@ mod test {
pub fn dot_product_function() {
let vector_a = DVec2::new(1., 2.);
let vector_b = DVec2::new(3., 4.);
assert_eq!(dot_product((), vector_a, vector_b), 11.);
assert_eq!(dot_product((), vector_a, vector_b, false), 11.);
}
#[test]

View file

@ -574,7 +574,6 @@ fn vibrance<T: Adjust<Color>>(
image
}
/// Color Channel
#[repr(u32)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, BufferStruct, FromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
@ -586,7 +585,6 @@ pub enum RedGreenBlue {
Blue,
}
/// Color Channel
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType, bytemuck::NoUninit, BufferStruct, FromPrimitive, IntoPrimitive)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Radio)]
@ -599,7 +597,7 @@ pub enum RedGreenBlueAlpha {
Alpha,
}
/// Style of noise pattern
/// Style of noise pattern.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)]
@ -616,9 +614,9 @@ pub enum NoiseType {
WhiteNoise,
}
/// Style of layered levels of the noise pattern.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
/// Style of layered levels of the noise pattern
pub enum FractalType {
#[default]
None,
@ -632,7 +630,7 @@ pub enum FractalType {
DomainWarpIndependent,
}
/// Distance function used by the cellular noise
/// Distance function used by the cellular noise.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
pub enum CellularDistanceFunction {
@ -663,7 +661,6 @@ pub enum CellularReturnType {
Division,
}
/// Type of domain warp
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, node_macro::ChoiceType)]
#[cfg_attr(feature = "std", derive(dyn_any::DynAny, specta::Type, serde::Serialize, serde::Deserialize))]
#[widget(Dropdown)]