Units
Units are named collections of pure functions, available globally across all components and callable directly from bind expressions and text interpolations. Define a utility once, use it anywhere.
Why Units Exist
Utility functions belong in one place, not scattered across every component that needs them. Units let you define formatting helpers, math operations, or string utilities once and call them from any component's template — without adding methods to individual abstractions just to make them reachable.
This is needed because WakaPAC's expression parser runs in an isolated scope: it evaluates bind expressions and text interpolations against the component's abstraction, not the JavaScript global scope. Units are the deliberate bridge — a clean, explicit way to expose shared functions to templates without opening the parser to arbitrary global access.
How Units Work
Units follow the Pascal uses convention: each unit defines its own namespace, and components can opt in to flat access via data-pac-uses. A unit lives in its own JS file and is registered on the page via wakaPAC.use():
<script src="waka-unit-math.js"></script>
<script>
wakaPAC.use(wakaMath);
</script>
wakapac.js and before the page initializes its components. Use conventional <script> tags in document order.
Namespaced Calls
Once registered, a unit's functions are available in any bind expression or text interpolation using UnitName.function() syntax. No component-level declaration is required.
{{ Math.round(price) }}
{{ StringUtils.capitalize(name) }}
click bindings, a bare reference like click: Stdlib.beep resolves to the function itself rather than calling it, and WakaPAC will invoke it with the DOM event as the first argument — which is unlikely to be what you want. Always write click: Stdlib.beep().
Flat Calls via data-pac-uses
Declare data-pac-uses on a component container to import a unit's functions into its flat namespace, dropping the unit prefix entirely. Useful when a unit is used heavily throughout a component's template.
<div data-pac-id="myComponent" data-pac-uses="StringUtils">
{{ truncate(title, 50) }}
{{ capitalize(name) }}
</div>
Import multiple units by separating their names with commas:
<div data-pac-id="myComponent" data-pac-uses="StringUtils, WakaMath">
{{ truncate(title, 50) }}
{{ round(price) }}
</div>
data-pac-uses wins. Use namespaced calls to disambiguate.
Variable Shadowing
A data property on the abstraction takes precedence over a registered unit of the same name. If the abstraction defines a StringUtils property, StringUtils.truncate() in a template resolves to that property — not the unit.
// Abstraction with a property named 'StringUtils'
{
StringUtils: {
truncate: () => 'I am a data property'
}
}
// In template: StringUtils.truncate() → calls the data property, not the unit
WakaPAC logs a console warning when a data property shadows a registered unit name, making the conflict easy to diagnose.
Writing a Unit
Unit functions receive pre-evaluated plain JavaScript values as arguments — no toolkit knowledge required. Wrap the unit in an IIFE, assign it to window, and implement createPacPlugin to return the unit's name and functions:
(function() {
"use strict";
window.MyUnit = {
createPacPlugin(pac, options = {}) {
return {
name: 'MyUnit',
functions: {
double: (n) => n * 2,
greet: (name) => 'Hello, ' + name + '!'
}
};
}
};
})();
Then register it on the page after loading the unit file:
wakaPAC.use(MyUnit);
Units that require configuration accept an options object as a second argument to wakaPAC.use(). The options are forwarded to createPacPlugin as the second parameter:
wakaPAC.use(DateUtils, { locale: 'nl-NL' });
Using Units as Helper Libraries
Units can also be used as imperative helper libraries within component methods, not just in templates. Call wakaPAC.unit() to retrieve a unit's function set by name or by library reference — both forms return the same object.
const cu = wakaPAC.unit('CollectionUtils');
const cu = wakaPAC.unit(CollectionUtils); // equivalent
The returned object is the same functions object the unit exposes to templates, so all functions are immediately available:
wakaPAC('#myComponent', {
init() {
const str = wakaPAC.unit('StringUtils');
console.log(str.truncate(this.title, 50));
console.log(str.slug(this.name));
}
});
Returns null if the unit name is not registered.
Supplied Units
WakaPAC ships with Stdlib built in and a set of optional unit files for common use cases. Optional units are standalone files that must be loaded and registered explicitly.
Stdlib
General-purpose utility functions under the Stdlib namespace. Stdlib is built into wakapac.js and registered automatically — no separate file or wakaPAC.use() call required.
| Function | Signature | Description |
|---|---|---|
log | log(...args) | Logs values to the browser console. Useful for inspecting abstraction state from within bind expressions. |
beep | beep(frequency=440, duration=200, volume=0.5, type='square') | Plays an audible tone via the Web Audio API. Requires a prior user gesture. Oscillator type: sine, square, sawtooth, or triangle. |
sendMessage | sendMessage(pacId, message, wParam=0, lParam=0, extended={}) | Sends a message to a specific WakaPAC component by id. Equivalent to wakaPAC.sendMessage() but callable from bind expressions. |
sendMessageToParent | sendMessageToParent(pacId, message, wParam=0, lParam=0, extended={}) | Sends a message to the parent component of the given container. Equivalent to wakaPAC.sendMessageToParent() but callable from bind expressions. |
broadcastMessage | broadcastMessage(message, wParam=0, lParam=0, extended={}) | Broadcasts a message to all active WakaPAC components. Equivalent to wakaPAC.broadcastMessage() but callable from bind expressions. |
log and beep are side-effect functions and not meaningful in {{ }} interpolations. Use them in event bindings: data-pac-bind="click: Stdlib.log(price)". As with all unit functions in click bindings, parentheses are required.
Math (waka-unit-math.js)
Exposes common mathematical operations under the Math namespace.
wakaPAC.use(WakaMath);
| Function | Signature | Description |
|---|---|---|
abs | abs(n) | Absolute value |
ceil | ceil(n) | Round up to nearest integer |
floor | floor(n) | Round down to nearest integer |
round | round(n) | Round to nearest integer |
min | min(...args) | Smallest of the given values |
max | max(...args) | Largest of the given values |
clamp | clamp(n, lo, hi) | Clamp n between lo and hi |
pow | pow(base, exp) | Base raised to the power of exp |
sqrt | sqrt(n) | Square root |
sign | sign(n) | Sign of a number: 1, -1, or 0 |
trunc | trunc(n) | Integer part, discarding fractional digits |
PI | PI() | Value of π |
random | random() | Pseudo-random number between 0 and 1. Re-evaluates on every render cycle — use sparingly in binds. |
roundTo | roundTo(n, decimals) | Round to a given number of decimal places |
inRange | inRange(n, lo, hi) | Returns true if n is within the inclusive range [lo, hi] |
toRad | toRad(deg) | Converts degrees to radians. |
toDeg | toDeg(rad) | Converts radians to degrees. |
StringUtils (waka-unit-stringutils.js)
Exposes common string operations under the StringUtils namespace.
wakaPAC.use(StringUtils);
| Function | Signature | Description |
|---|---|---|
capitalize | capitalize(s) | First character uppercased, rest lowercased |
upper | upper(s) | Convert to uppercase |
lower | lower(s) | Convert to lowercase |
trim | trim(s) | Remove leading and trailing whitespace |
truncate | truncate(s, max, ellipsis='…') | Truncate to max characters, appending ellipsis if cut |
replace | replace(s, search, replacement) | Replace first occurrence of substring |
replaceAll | replaceAll(s, search, replacement) | Replace all occurrences of substring |
startsWith | startsWith(s, search) | Returns true if string starts with search |
endsWith | endsWith(s, search) | Returns true if string ends with search |
contains | contains(s, search) | Returns true if string contains search |
padStart | padStart(s, length, fill=' ') | Pad the start to target length |
padEnd | padEnd(s, length, fill=' ') | Pad the end to target length |
repeat | repeat(s, count) | Repeat string count times |
length | length(s) | Number of characters in the string |
slug | slug(s) | Convert to URL-friendly slug |
countStr | countStr(substr, s) | Number of non-overlapping occurrences of substr in s (case-sensitive) |
containsText | containsText(s, substr) | Returns true if s contains substr, ignoring case |
startsText | startsText(s, substr) | Returns true if s starts with substr, ignoring case |
endsText | endsText(s, substr) | Returns true if s ends with substr, ignoring case |
reverseString | reverseString(s) | Reverses the characters in a string |
DateUtils (waka-unit-dateutils.js)
Exposes date formatting, arithmetic, and comparison operations under the DateUtils namespace. Backed entirely by the native Intl API — no external libraries required. Accepts an optional locale at registration time; defaults to the browser's locale.
wakaPAC.use(DateUtils); // browser default locale
wakaPAC.use(DateUtils, { locale: 'nl-NL' }); // explicit locale
All functions accept a Date object, a numeric timestamp, or a date string. Invalid or missing input returns '' or null rather than throwing.
| Function | Signature | Description |
|---|---|---|
formatShort | formatShort(date) | Short locale date string (e.g. "29-3-2026") |
formatMedium | formatMedium(date) | Medium locale date string (e.g. "29 mrt 2026") |
formatLong | formatLong(date) | Long locale date string (e.g. "29 maart 2026") |
formatDateTime | formatDateTime(date) | Medium date with short time (e.g. "29 mrt 2026 14:05") |
formatTime | formatTime(date) | Time only (e.g. "14:05") |
fromNow | fromNow(date) | Relative time string (e.g. "3 days ago", "in 2 hours") |
year | year(date) | Full year as integer (e.g. 2026) |
month | month(date) | Month as 1-based integer (1–12) |
day | day(date) | Day of month (1–31) |
monthName | monthName(date) | Locale-aware month name (e.g. "maart") |
weekday | weekday(date) | Locale-aware weekday name (e.g. "zondag") |
today | today() | Today's date at midnight |
now | now() | Current date and time. Re-evaluates on every render cycle — use sparingly in binds. |
isPast | isPast(date) | Returns true if the date is in the past |
isFuture | isFuture(date) | Returns true if the date is in the future |
isSameDay | isSameDay(a, b) | Returns true if two dates fall on the same calendar day |
isWeekend | isWeekend(date) | Returns true if the date falls on Saturday or Sunday |
isBetween | isBetween(date, start, end) | Returns true if date falls between start and end (inclusive) |
diffDays | diffDays(a, b) | Difference in whole days between two dates. Positive if b is after a. |
addDays | addDays(date, n) | New date with n days added (negative to subtract) |
addMonths | addMonths(date, n) | New date with n months added, clamped to last valid day |
addYears | addYears(date, n) | New date with n years added, clamped to last valid day |
startOf | startOf(date, unit) | Start of 'day', 'week', 'month', or 'year' (00:00:00.000). Week starts Monday. |
endOf | endOf(date, unit) | End of 'day', 'week', 'month', or 'year' (23:59:59.999). Week ends Sunday. |
weekNumber | weekNumber(date) | ISO week number of the year (1–53) |
quarter | quarter(date) | Quarter of the year (1–4) |
yearsBetween | yearsBetween(a, b) | Whole years between two dates. Positive if b is after a. |
monthsBetween | monthsBetween(a, b) | Whole months between two dates. Positive if b is after a. |
weeksBetween | weeksBetween(a, b) | Whole weeks between two dates. Positive if b is after a. |
TypeUtils (waka-unit-typeutils.js)
Exposes type checking and type coercion functions under the TypeUtils namespace. Useful in bind conditions where typeof and strict equality checks are not available in template expressions.
wakaPAC.use(TypeUtils);
| Function | Signature | Description |
|---|---|---|
| Type Checking | ||
isNull | isNull(value) | Returns true if value is null or undefined |
isDefined | isDefined(value) | Returns true if value is not null and not undefined |
isString | isString(value) | Returns true if value is a string |
isNumber | isNumber(value) | Returns true if value is a finite number (excludes NaN and Infinity) |
isInt | isInt(value) | Returns true if value is an integer |
isBool | isBool(value) | Returns true if value is a boolean |
isArray | isArray(value) | Returns true if value is an array |
isObject | isObject(value) | Returns true if value is a plain object (not array, Date, or null) |
isDate | isDate(value) | Returns true if value is a valid Date object |
isFunction | isFunction(value) | Returns true if value is a function |
| Emptiness | ||
isEmpty | isEmpty(value) | Returns true for null, undefined, empty string, empty array, object with no own keys, or 0 |
isNaN | isNaN(value) | Returns true if value is NaN |
| Type Coercion | ||
toInt | toInt(value, radix=10) | Convert to integer; returns null if not parseable |
toFloat | toFloat(value) | Convert to float; returns null if not parseable |
toBool | toBool(value) | Convert to boolean. Strings 'true', '1', 'yes', 'on' → true; 'false', '0', 'no', 'off' → false |
toString | toString(value) | Convert to string; null and undefined return '' rather than 'null'/'undefined' |
coalesce | coalesce(...args) | Returns the first non-null, non-undefined argument |
typeOf | typeOf(value) | Returns type as string: 'null', 'array', 'date', or standard typeof values |
PhpUtils (waka-unit-phputils.js)
Exposes a curated set of PHP-style utility functions under the PhpUtils namespace. Functions follow PHP naming conventions and signatures. in_array always uses strict comparison (===).
wakaPAC.use(PhpUtils);
| Function | Signature | Description |
|---|---|---|
| String | ||
nl2br | nl2br(s) | Insert <br> before each newline |
wordwrap | wordwrap(s, width=75, breakStr='\n', cutLongWords=false) | Wrap string to given character width |
str_pad | str_pad(s, length, padStr=' ', type=0) | Pad string to length. type: 0 = right, 1 = left, 2 = both |
str_repeat | str_repeat(s, times) | Repeat string times times |
str_word_count | str_word_count(s) | Number of words in a string |
ucwords | ucwords(s) | Uppercase first character of each word |
lcfirst | lcfirst(s) | Lowercase first character of a string |
| Number | ||
number_format | number_format(n, decimals=0, decPoint='.', thousandsSep=',') | Format number with grouped thousands and decimal point |
| Array | ||
implode | implode(glue, arr) | Join array elements into a string |
count | count(value) | Number of elements in an array or string, or own keys of an object |
in_array | in_array(needle, haystack) | Returns true if needle exists in haystack (strict comparison) |
array_key_exists | array_key_exists(key, value) | Returns true if key exists in array or object |
NumberUtils (waka-unit-numberutils.js)
Exposes number formatting operations under the NumberUtils namespace. Backed entirely by the native Intl API — no external libraries required. Accepts an optional locale at registration time; defaults to the browser's locale.
wakaPAC.use(NumberUtils); // browser default locale
wakaPAC.use(NumberUtils, { locale: 'nl-NL' }); // explicit locale
percent() expects a fractional value — pass 0.75 to get "75%", not 75. This matches the Intl.NumberFormat percent style convention.
| Function | Signature | Description |
|---|---|---|
format | format(n, decimals=0) | Locale-formatted number with grouped thousands (e.g. "1.200" in nl-NL) |
currency | currency(n, currency='EUR', display='symbol') | Currency value. display: 'symbol' (€), 'code' (EUR), or 'name' (euro) |
percent | percent(n, decimals=0) | Percentage from fractional input (0.75 → "75%") |
compact | compact(n, display='short') | Compact notation (e.g. 1400 → "1.4K"). display: 'short' or 'long' |
duration | duration(totalSeconds) | Human-readable duration (e.g. 3661 → "1h 01m 01s"). Leading zero units omitted. |
CollectionUtils (waka-unit-collectionutils.js)
Exposes array and collection operations under the CollectionUtils namespace. Key paths support dot-notation for nested property access (e.g. 'address.city').
wakaPAC.use(CollectionUtils);
| Function | Signature | Description |
|---|---|---|
groupBy | groupBy(arr, key) | Groups items by the value at key path. Returns an object keyed by group value. |
sortBy | sortBy(arr, key, dir='asc') | Returns a new array sorted by key path. dir: 'asc' or 'desc'. |
unique | unique(arr, key?) | Removes duplicates. Without key uses strict equality; with key deduplicates by that property. |
countBy | countBy(arr, key) | Returns an object mapping each distinct value at key to its count. |
chunk | chunk(arr, size) | Splits array into chunks of the given size. |
flatten | flatten(arr) | Flattens one level of nesting. |
zip | zip(a, b) | Interleaves two arrays into [a, b] pairs, stopping at the shorter length. |
sum | sum(arr, key) | Sum of values at key path. Non-numeric values skipped. |
avg | avg(arr, key) | Average of values at key path. Returns null if no numeric values found. |
min | min(arr, key) | Minimum value at key path. Returns null if no numeric values found. |
max | max(arr, key) | Maximum value at key path. Returns null if no numeric values found. |
RegexUtils (waka-unit-regexutils.js)
Exposes regular expression operations under the RegexUtils namespace. Patterns are parsed and cached on first use — no performance cost on repeated evaluations across render cycles.
wakaPAC.use(RegexUtils);
Patterns can be plain strings or slash-delimited with flags:
'foo' // plain pattern, no flags
'/foo/i' // case-insensitive
'/^\d+$/m' // anchored, multiline
| Function | Signature | Description |
|---|---|---|
test | test(pattern, s) | Returns true if the pattern matches anywhere in the string |
match | match(pattern, s) | Returns the first match string, or null if not found |
extract | extract(pattern, s, group=1) | Returns the value of a capture group. Group 0 = full match, 1 = first capture group. |
replace | replace(pattern, s, replacement) | Replaces the first match. If the pattern includes the g flag, all matches are replaced. |
replaceAll | replaceAll(pattern, s, replacement) | Replaces all matches. Forces the g flag regardless of the pattern. |
ColorUtils (waka-unit-colorutils.js)
Exposes color manipulation functions under the ColorUtils namespace. All functions accept hex color strings in #rgb or #rrggbb format and return empty string on invalid input rather than throwing.
wakaPAC.use(ColorUtils);
Blend amounts are fractional values between 0 and 1, where 0 leaves the color unchanged and 1 produces the pure target color (white for lighten, black for darken).
| Function | Signature | Description |
|---|---|---|
lighten | lighten(hex, amount=0.2) | Mix the color toward white by the given ratio. Returns a hex string. |
darken | darken(hex, amount=0.2) | Mix the color toward black by the given ratio. Returns a hex string. |
alpha | alpha(hex, alpha=1) | Return an rgba() CSS string with the given alpha (0–1). |
hexToRgb | hexToRgb(hex) | Convert a hex color to an {r, g, b} object, or '' if invalid. |
rgbToHex | rgbToHex(r, g, b) | Convert RGB channel values (0–255) to a hex color string. Channel values are clamped. |
EscapeUtils (waka-unit-escapeutils.js)
Exposes string escaping and sanitization functions under the EscapeUtils namespace. Useful when inserting user-supplied content into HTML output, regular expressions, or URL query strings. null and undefined are coerced to an empty string rather than the literal text "null".
wakaPAC.use(EscapeUtils);
| Function | Signature | Description |
|---|---|---|
escapeHtml | escapeHtml(value) | Escape &, <, >, ", and ' to their HTML entities. |
unescapeHtml | unescapeHtml(value) | Convert HTML entities back to their literal characters. |
escapeRegex | escapeRegex(value) | Escape all regex metacharacters so the string can be embedded safely in a RegExp. |
escapeUrl | escapeUrl(value) | Percent-encode a value for use as a URL component (wraps encodeURIComponent). |
unescapeUrl | unescapeUrl(value) | Decode a percent-encoded URL component. Returns '' on malformed input instead of throwing. |
Math3D (waka-unit-math3d.js)
Exposes 4×4 matrix math, 3-component vector operations, and quaternion support under the Math3D namespace. All matrices are column-major Float32Array instances (16 elements), compatible with gl.uniformMatrix4fv() without transposing. Vectors are plain number[] arrays [x, y, z]. Quaternions are plain number[] arrays [x, y, z, w].
Float32Array objects intended for WebGL uniform calls, not for rendering into the DOM. Use this unit imperatively inside component init() or event handlers, not in {{ }} interpolations or data-pac-bind attributes.
wakaPAC.use(Math3D);
// Retrieve in a component method
const m = wakaPAC.unit(Math3D);
// Build an MVP matrix
const mvp = m.mul(m.perspective(fov, aspect, near, far),
m.mul(m.translation(0, 0, -4),
m.mul(m.rotX(rx), m.rotY(ry))));
gl.uniformMatrix4fv(uMVP, false, mvp);
// Normal matrix for lighting (corrects normals under non-uniform scale)
const normalMatrix = m.transpose(m.invert(modelMatrix));
gl.uniformMatrix4fv(uNormal, false, normalMatrix);
// Smooth rotation via quaternion slerp
const q = m.quatSlerp(qA, qB, t);
gl.uniformMatrix4fv(uRot, false, m.fromQuat(q));
MAT4 — 4×4 matrix operations
| Function | Signature | Description |
|---|---|---|
identity | identity() | Returns a new 4×4 identity matrix. |
mul | mul(a, b) | Returns a × b. Transforms apply right-to-left: mul(view, model) applies model first. |
invert | invert(m) | Returns the inverse of m, or null if the matrix is singular (determinant ≈ 0). Always null-check the result before passing to gl.uniformMatrix4fv(). Used with transpose() to produce normal matrices for lighting. |
transpose | transpose(m) | Returns the transpose of m (rows and columns swapped). |
perspective | perspective(fovY, aspect, near, far) | Perspective projection matrix. fovY is vertical field of view in radians. aspect is width / height. near must be > 0. |
ortho | ortho(left, right, bottom, top, near, far) | Orthographic projection matrix. No foreshortening — useful for 2D overlays and UI elements. |
frustum | frustum(left, right, bottom, top, near, far) | Frustum projection matrix. Lower-level alternative to perspective() for asymmetric frustums (e.g. VR, off-axis projections). |
translation | translation(x, y, z) | Translation matrix. Moves geometry by (x, y, z) in world space. |
scale | scale(x, y, z) | Scale matrix. Non-uniform scale changes surface normals — pair with a normal matrix in shaders. |
rotX | rotX(angle) | Rotation matrix around the X axis. angle in radians. |
rotY | rotY(angle) | Rotation matrix around the Y axis. angle in radians. |
rotZ | rotZ(angle) | Rotation matrix around the Z axis. angle in radians. |
lookAt | lookAt(eye, target, up) | View matrix from camera position, look-at target, and up hint (typically [0,1,0]). All three are [x,y,z] arrays. If the forward direction is parallel to the up hint (camera looking straight up or down), a fallback up vector is used silently — the camera keeps working but its roll orientation is arbitrary. |
fromQuat | fromQuat(q) | Rotation matrix from a unit quaternion [x,y,z,w]. Use with quatSlerp() for smooth interpolated rotations. |
mat4Clone | mat4Clone(m) | Returns a deep copy of a 4×4 matrix. Float32Arrays are reference types — use this before branching transforms from a shared base matrix to avoid corrupting the original. |
VEC3 — 3-component vector operations
| Function | Signature | Description |
|---|---|---|
vec3Add | vec3Add(a, b) | Returns a + b. |
vec3Subtract | vec3Subtract(a, b) | Returns a − b. |
vec3Scale | vec3Scale(v, s) | Returns v scaled by scalar s. |
vec3Dot | vec3Dot(a, b) | Dot product of a and b. Returns a scalar. |
vec3Cross | vec3Cross(a, b) | Cross product a × b. Result is perpendicular to both inputs. |
vec3Length | vec3Length(v) | Euclidean length of v. |
vec3Normalize | vec3Normalize(v) | Unit-length version of v. Returns [0,0,0] for zero-length input. |
vec3Lerp | vec3Lerp(a, b, t) | Linearly interpolates between a and b. t=0 returns a, t=1 returns b. |
vec3Distance | vec3Distance(a, b) | Euclidean distance between two points. |
QUAT — quaternion operations
Quaternions are [x, y, z, w] arrays where (x, y, z) is the imaginary vector part and w is the real scalar. Unit quaternions (length = 1) represent 3D rotations without gimbal lock.
| Function | Signature | Description |
|---|---|---|
quatIdentity | quatIdentity() | Returns the identity quaternion [0,0,0,1] — no rotation. |
quatFromEuler | quatFromEuler(x, y, z) | Quaternion from Euler angles in radians, applied in X → Y → Z order. |
quatFromAxisAngle | quatFromAxisAngle(axis, angle) | Quaternion for a rotation of angle radians around axis. The axis need not be unit length. |
quatMul | quatMul(a, b) | Returns a × b (Hamilton product). Applies rotation b first, then a — same right-to-left convention as matrix multiplication. |
quatNormalize | quatNormalize(q) | Returns the unit-length version of q. Call periodically to correct floating-point drift from repeated quatMul calls. Returns identity for near-zero input. |
quatConjugate | quatConjugate(q) | Negates the vector part (x, y, z), leaving w unchanged. For unit quaternions this equals the inverse and represents the opposite rotation. Prefer this over quatInvert() whenever the quaternion is known to be unit length — it is a simple negation with no division. |
quatInvert | quatInvert(q) | Full quaternion inverse: conjugate divided by squared length. Use only when the quaternion may not be unit length. For unit quaternions, quatConjugate() is equivalent and cheaper. Returns identity for near-zero input. |
quatSlerp | quatSlerp(a, b, t) | Spherical interpolation between two unit quaternions. Produces smooth, constant-speed rotation without gimbal lock. t=0 returns a, t=1 returns b. |