Reactive mathematical utility functions and composables for Vue.js applications
—
Functions for mapping values between different numerical ranges, useful for data visualization, animations, and value transformations. These utilities provide both numeric and generic type-safe projection capabilities.
/**
* Function that projects input from one domain to another
* @param input - The input value to project
* @param from - Source domain as [min, max] tuple
* @param to - Target domain as [min, max] tuple
* @returns Projected value in target domain
*/
type ProjectorFunction<F, T> = (
input: F,
from: readonly [F, F],
to: readonly [T, T]
) => T;
/**
* Function type returned by projection creators
* @param input - Reactive input value to project
* @returns ComputedRef containing projected value
*/
type UseProjection<F, T> = (input: MaybeRefOrGetter<F>) => ComputedRef<T>;Creates a reusable numeric projection function for mapping values between number ranges with an optional custom projector.
/**
* Creates a numeric projection function for mapping between ranges
* @param fromDomain - Source range as [min, max] (reactive)
* @param toDomain - Target range as [min, max] (reactive)
* @param projector - Optional custom projection function
* @returns Projection function that can be called with input values
*/
function createProjection(
fromDomain: MaybeRefOrGetter<readonly [number, number]>,
toDomain: MaybeRefOrGetter<readonly [number, number]>,
projector?: ProjectorFunction<number, number>
): UseProjection<number, number>;Usage Examples:
import { ref } from "vue";
import { createProjection } from "@vueuse/math";
// Create a projection from 0-100 to 0-1
const percentToDecimal = createProjection([0, 100], [0, 1]);
const percentage = ref(75);
const decimal = percentToDecimal(percentage);
console.log(decimal.value); // 0.75
percentage.value = 50;
console.log(decimal.value); // 0.5
// Create projection with reactive domains
const minInput = ref(0);
const maxInput = ref(1024);
const minOutput = ref(0);
const maxOutput = ref(255);
const inputRange = computed(() => [minInput.value, maxInput.value] as const);
const outputRange = computed(() => [minOutput.value, maxOutput.value] as const);
const scaleDown = createProjection(inputRange, outputRange);
const highResValue = ref(512);
const lowResValue = scaleDown(highResValue);
console.log(lowResValue.value); // 127.5
// Change the output range
maxOutput.value = 100;
console.log(lowResValue.value); // ~50 (automatically recalculated)
// Temperature conversion (Celsius to Fahrenheit)
const celsiusToFahrenheit = createProjection(
[0, 100], // Celsius range (water freezing to boiling)
[32, 212], // Fahrenheit range
);
const celsius = ref(25);
const fahrenheit = celsiusToFahrenheit(celsius);
console.log(fahrenheit.value); // 77°FCreates a generic projection function that can work with any types, not just numbers, using a custom projector function.
/**
* Creates a generic typed projection function for any domain types
* @param fromDomain - Source range as [min, max] (reactive)
* @param toDomain - Target range as [min, max] (reactive)
* @param projector - Custom projection function for the specific types
* @returns Generic projection function
*/
function createGenericProjection<F = number, T = number>(
fromDomain: MaybeRefOrGetter<readonly [F, F]>,
toDomain: MaybeRefOrGetter<readonly [T, T]>,
projector: ProjectorFunction<F, T>
): UseProjection<F, T>;Usage Examples:
import { ref } from "vue";
import { createGenericProjection } from "@vueuse/math";
// Color interpolation between RGB values
type RGB = { r: number; g: number; b: number };
const colorProjector = (
input: number,
from: readonly [number, number],
to: readonly [RGB, RGB]
): RGB => {
const progress = (input - from[0]) / (from[1] - from[0]);
const [startColor, endColor] = to;
return {
r: Math.round(startColor.r + (endColor.r - startColor.r) * progress),
g: Math.round(startColor.g + (endColor.g - startColor.g) * progress),
b: Math.round(startColor.b + (endColor.b - startColor.b) * progress),
};
};
const interpolateColor = createGenericProjection(
[0, 100],
[{ r: 255, g: 0, b: 0 }, { r: 0, g: 255, b: 0 }], // Red to Green
colorProjector
);
const progress = ref(50);
const currentColor = interpolateColor(progress);
console.log(currentColor.value); // { r: 128, g: 128, b: 0 } (yellow)
// String interpolation example
const stringProjector = (
input: number,
from: readonly [number, number],
to: readonly [string, string]
): string => {
const progress = (input - from[0]) / (from[1] - from[0]);
const [start, end] = to;
const index = Math.round(progress * (end.length - start.length)) + start.length;
return start + end.slice(start.length, index);
};
const expandString = createGenericProjection(
[0, 10],
["Hello", "Hello World!"],
stringProjector
);
const step = ref(5);
const message = expandString(step);
console.log(message.value); // "Hello Wor"Direct numeric projection function that immediately applies projection to a value without creating a reusable projector.
/**
* Directly project a value between numeric ranges
* @param input - The value to project (reactive)
* @param fromDomain - Source range as [min, max] (reactive)
* @param toDomain - Target range as [min, max] (reactive)
* @param projector - Optional custom projection function
* @returns ComputedRef containing the projected value
*/
function useProjection(
input: MaybeRefOrGetter<number>,
fromDomain: MaybeRefOrGetter<readonly [number, number]>,
toDomain: MaybeRefOrGetter<readonly [number, number]>,
projector?: ProjectorFunction<number, number>
): ComputedRef<number>;Usage Examples:
import { ref } from "vue";
import { useProjection } from "@vueuse/math";
// Direct projection usage
const temperature = ref(20); // Celsius
const fahrenheit = useProjection(
temperature,
[0, 100], // Celsius range
[32, 212] // Fahrenheit range
);
console.log(fahrenheit.value); // 68°F
// Slider to progress bar
const sliderValue = ref(750);
const progressPercent = useProjection(
sliderValue,
[0, 1000], // Slider range
[0, 100] // Percentage range
);
console.log(progressPercent.value); // 75%
// Reactive domains
const minTemp = ref(-10);
const maxTemp = ref(40);
const currentTemp = ref(20);
const comfort = useProjection(
currentTemp,
computed(() => [minTemp.value, maxTemp.value]),
[0, 100] // Comfort index 0-100
);
console.log(comfort.value); // 60 (comfort index)
// Custom projector for non-linear scaling
const exponentialProjector = (
input: number,
from: readonly [number, number],
to: readonly [number, number]
): number => {
const normalizedInput = (input - from[0]) / (from[1] - from[0]);
const exponential = Math.pow(normalizedInput, 2); // Square for exponential curve
return to[0] + (to[1] - to[0]) * exponential;
};
const linearInput = ref(50);
const exponentialOutput = useProjection(
linearInput,
[0, 100],
[0, 1000],
exponentialProjector
);
console.log(exponentialOutput.value); // 250 (0.5² * 1000)import { ref, computed } from "vue";
import { createProjection, useProjection } from "@vueuse/math";
// Create easing projection
const easeInOut = (
input: number,
from: readonly [number, number],
to: readonly [number, number]
): number => {
const t = (input - from[0]) / (from[1] - from[0]);
const eased = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
return to[0] + (to[1] - to[0]) * eased;
};
const animationProgress = ref(0); // 0 to 1
const easedPosition = useProjection(
animationProgress,
[0, 1],
[0, 500], // 500px movement
easeInOut
);
// Animate
function animate() {
animationProgress.value += 0.01;
console.log(`Position: ${easedPosition.value}px`);
if (animationProgress.value < 1) {
requestAnimationFrame(animate);
}
}import { ref, computed } from "vue";
import { createProjection } from "@vueuse/math";
// Chart scaling
const data = ref([10, 25, 15, 30, 20, 35, 40]);
const chartHeight = ref(400);
const chartPadding = ref(20);
// Find data range
const dataMin = computed(() => Math.min(...data.value));
const dataMax = computed(() => Math.max(...data.value));
// Create Y-axis projection
const dataToPixels = createProjection(
computed(() => [dataMin.value, dataMax.value]),
computed(() => [chartHeight.value - chartPadding.value, chartPadding.value])
);
// Convert data points to pixel positions
const pixelPositions = computed(() =>
data.value.map(value => dataToPixels(ref(value)).value)
);
console.log(pixelPositions.value);
// [380, 220, 300, 140, 260, 100, 20] (inverted Y for SVG/Canvas)
// X-axis projection for spacing
const indexToPixels = createProjection(
[0, data.value.length - 1],
[chartPadding.value, 800 - chartPadding.value]
);
const xPositions = computed(() =>
data.value.map((_, index) => indexToPixels(ref(index)).value)
);import { ref, computed } from "vue";
import { createProjection } from "@vueuse/math";
// Create different projections for different ranges
const input = ref(0);
// Conditional projection based on input range
const complexProjection = computed(() => {
const val = input.value;
if (val <= 50) {
// Linear projection for 0-50
const linearProj = createProjection([0, 50], [0, 100]);
return linearProj(input).value;
} else {
// Logarithmic projection for 50-100
const logProj = createProjection(
[50, 100],
[100, 1000],
(input, from, to) => {
const normalized = (input - from[0]) / (from[1] - from[0]);
const logValue = Math.log10(1 + normalized * 9); // log10(1 to 10)
return to[0] + (to[1] - to[0]) * logValue;
}
);
return logProj(input).value;
}
});
// Test different input ranges
input.value = 25;
console.log(complexProjection.value); // 50 (linear)
input.value = 75;
console.log(complexProjection.value); // ~397 (logarithmic)Install with Tessl CLI
npx tessl i tessl/npm-vueuse--math