diff --git a/src/core/utils.ts b/src/core/utils.ts index 68ed4e3..da56c56 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -30,7 +30,7 @@ export const isFunction = (obj: unknown): obj is Function => export function isObject( item: unknown, ): item is Record { - return typeof item === 'object'; + return item !== null && typeof item === 'object'; } export function isArray(item: unknown): item is any[] { @@ -99,9 +99,9 @@ export function flattenStyles( result: Styles = {}, ): Styles { if (isArray(obj)) { - obj.forEach((item) => { - flattenStyles(item, result); - }); + for (let i = 0; i < obj.length; i++) { + flattenStyles(obj[i], result); + } } else if (obj) { // handle the case where the object is not an array for (const key in obj) { diff --git a/src/primitives/utils/chainFunctions.ts b/src/primitives/utils/chainFunctions.ts index 6799279..15e6b3e 100644 --- a/src/primitives/utils/chainFunctions.ts +++ b/src/primitives/utils/chainFunctions.ts @@ -23,22 +23,42 @@ export function chainFunctions( export function chainFunctions( ...fns: (AnyFunction | undefined | null | false)[] ): AnyFunction | undefined { - const onlyFunctions = fns.filter((func) => typeof func === 'function'); - if (onlyFunctions.length === 0) { - return undefined; + // Inline filter to avoid the intermediate array allocation when most + // callers pass 2 args and one or both happen to be falsy. + let first: AnyFunction | undefined; + let onlyFunctions: AnyFunction[] | undefined; + for (let i = 0; i < fns.length; i++) { + const fn = fns[i]; + if (typeof fn !== 'function') continue; + if (first === undefined) { + first = fn; + } else { + if (onlyFunctions === undefined) onlyFunctions = [first]; + onlyFunctions.push(fn); + } } - if (onlyFunctions.length === 1) { - return onlyFunctions[0]; + if (first === undefined) return undefined; + if (onlyFunctions === undefined) return first; + + // Fast path: exactly two functions — the common case for ref/handler + // forwarding (props.onX + local onX). Avoids the loop. + if (onlyFunctions.length === 2) { + const a = onlyFunctions[0]!; + const b = onlyFunctions[1]!; + return function (this: unknown, ...innerArgs) { + const result = a.apply(this, innerArgs); + if (result === true) return result; + return b.apply(this, innerArgs); + }; } - return function (...innerArgs) { + const chained = onlyFunctions; + return function (this: unknown, ...innerArgs) { let result; - for (const func of onlyFunctions) { - result = func.apply(this, innerArgs); - if (result === true) { - return result; - } + for (let i = 0; i < chained.length; i++) { + result = chained[i]!.apply(this, innerArgs); + if (result === true) return result; } return result; }; diff --git a/src/primitives/utils/createBlurredImage.ts b/src/primitives/utils/createBlurredImage.ts index dee55a1..7f7b284 100644 --- a/src/primitives/utils/createBlurredImage.ts +++ b/src/primitives/utils/createBlurredImage.ts @@ -100,32 +100,38 @@ function applyVerticalBlur( half: number, ): void { for (let y = 0; y < height; y++) { + const kStart = -y < -half ? -half : -y; + const kEnd = height - 1 - y < half ? height - 1 - y : half; + + // Kernel sums to 1; only edge rows need a renormalization factor. + let invWeight = 1; + if (kStart !== -half || kEnd !== half) { + let weightSum = 0; + for (let k = kStart; k <= kEnd; k++) weightSum += kernel[k + half]!; + invWeight = 1 / weightSum; + } + + const rowBase = y * width; for (let x = 0; x < width; x++) { let r = 0, g = 0, b = 0, a = 0; - let weightSum = 0; - for (let ky = -half; ky <= half; ky++) { - const py = y + ky; - if (py >= 0 && py < height) { - const pixelIndex = (py * width + x) * 4; - const weight = kernel[ky + half]!; - - r += input[pixelIndex]! * weight; - g += input[pixelIndex + 1]! * weight; - b += input[pixelIndex + 2]! * weight; - a += input[pixelIndex + 3]! * weight; - weightSum += weight; - } + for (let ky = kStart; ky <= kEnd; ky++) { + const pixelIndex = ((y + ky) * width + x) * 4; + const weight = kernel[ky + half]!; + r += input[pixelIndex]! * weight; + g += input[pixelIndex + 1]! * weight; + b += input[pixelIndex + 2]! * weight; + a += input[pixelIndex + 3]! * weight; } - const outputIndex = (y * width + x) * 4; - output[outputIndex] = r / weightSum; - output[outputIndex + 1] = g / weightSum; - output[outputIndex + 2] = b / weightSum; - output[outputIndex + 3] = a / weightSum; + const outputIndex = (rowBase + x) * 4; + output[outputIndex] = r * invWeight; + output[outputIndex + 1] = g * invWeight; + output[outputIndex + 2] = b * invWeight; + output[outputIndex + 3] = a * invWeight; } } } @@ -147,33 +153,46 @@ function applyHorizontalBlur( kernel: Readonly, half: number, ): void { + // Precompute per-column inverse weight sums; kernel sums to 1 in the interior. + const invWeights = new Float64Array(width); + for (let x = 0; x < width; x++) { + const kStart = -x < -half ? -half : -x; + const kEnd = width - 1 - x < half ? width - 1 - x : half; + if (kStart === -half && kEnd === half) { + invWeights[x] = 1; + } else { + let weightSum = 0; + for (let k = kStart; k <= kEnd; k++) weightSum += kernel[k + half]!; + invWeights[x] = 1 / weightSum; + } + } + for (let y = 0; y < height; y++) { + const rowBase = y * width; for (let x = 0; x < width; x++) { + const kStart = -x < -half ? -half : -x; + const kEnd = width - 1 - x < half ? width - 1 - x : half; + let r = 0, g = 0, b = 0, a = 0; - let weightSum = 0; - for (let kx = -half; kx <= half; kx++) { - const px = x + kx; - if (px >= 0 && px < width) { - const pixelIndex = (y * width + px) * 4; - const weight = kernel[kx + half]!; - - r += input[pixelIndex]! * weight; - g += input[pixelIndex + 1]! * weight; - b += input[pixelIndex + 2]! * weight; - a += input[pixelIndex + 3]! * weight; - weightSum += weight; - } + for (let kx = kStart; kx <= kEnd; kx++) { + const pixelIndex = (rowBase + x + kx) * 4; + const weight = kernel[kx + half]!; + r += input[pixelIndex]! * weight; + g += input[pixelIndex + 1]! * weight; + b += input[pixelIndex + 2]! * weight; + a += input[pixelIndex + 3]! * weight; } - const outputIndex = (y * width + x) * 4; - output[outputIndex] = r / weightSum; - output[outputIndex + 1] = g / weightSum; - output[outputIndex + 2] = b / weightSum; - output[outputIndex + 3] = a / weightSum; + const invWeight = invWeights[x]!; + const outputIndex = (rowBase + x) * 4; + output[outputIndex] = r * invWeight; + output[outputIndex + 1] = g * invWeight; + output[outputIndex + 2] = b * invWeight; + output[outputIndex + 3] = a * invWeight; } } } @@ -215,15 +234,14 @@ function gaussianBlurConvolution( ): ImageData { const { data } = imageData; const { width, height } = dimensions; + const tempData = new Uint8ClampedArray(data.length); const output = new Uint8ClampedArray(data.length); const kernelSize = Math.ceil(radius * 2) * 2 + 1; const kernel = generateGaussianKernel(kernelSize, radius); - const half = Math.floor(kernelSize / 2); - - applyHorizontalBlur(data, output, width, height, kernel, half); + const half = (kernelSize - 1) >> 1; - const tempData = new Uint8ClampedArray(output); + applyHorizontalBlur(data, tempData, width, height, kernel, half); applyVerticalBlur(tempData, output, width, height, kernel, half); return new ImageData(output, width, height); diff --git a/src/primitives/utils/handleNavigation.ts b/src/primitives/utils/handleNavigation.ts index 7d3fd47..587377d 100644 --- a/src/primitives/utils/handleNavigation.ts +++ b/src/primitives/utils/handleNavigation.ts @@ -220,10 +220,10 @@ export function moveSelection( return selectChild(el, selected); } -function distanceBetweenRectCenters(a: lng.Rect, b: lng.Rect): number { - const dx = Math.abs(a.x + a.width / 2 - (b.x + b.width / 2)) / 2; - const dy = Math.abs(a.y + a.height / 2 - (b.y + b.height / 2)) / 2; - return Math.sqrt(dx * dx + dy * dy); +function squaredDistanceBetweenRectCenters(a: lng.Rect, b: lng.Rect): number { + const dx = a.x + a.width / 2 - (b.x + b.width / 2); + const dy = a.y + a.height / 2 - (b.y + b.height / 2); + return dx * dx + dy * dy; } function findClosestFocusableChildIdx( @@ -238,12 +238,14 @@ function findClosestFocusableChildIdx( let closestIdx = -1; let closestDist = Infinity; - for (const [idx, child] of el.children.entries()) { + const children = el.children; + for (let idx = 0; idx < children.length; idx++) { + const child = children[idx]!; if (!child.skipFocus) { lng.getElementScreenRect(child, el, childRect); childRect.x += elRect.x; childRect.y += elRect.y; - const distance = distanceBetweenRectCenters(prevRect, childRect); + const distance = squaredDistanceBetweenRectCenters(prevRect, childRect); if (distance < closestDist) { closestDist = distance; closestIdx = idx; diff --git a/src/utils.ts b/src/utils.ts index 0236e62..a502aab 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,16 +19,18 @@ export function hexColor(color: string | number = ''): number { if (typeof color === 'string') { // Renderer expects RGBA values - if (color.startsWith('#')) { - return Number( - color.replace('#', '0x') + (color.length === 7 ? 'ff' : ''), - ); - } - - if (color.startsWith('0x')) { - return Number(color); + let hex: string; + if (color.charCodeAt(0) === 35 /* '#' */) { + hex = color.length === 7 ? color.slice(1) + 'ff' : color.slice(1); + } else if ( + color.charCodeAt(0) === 48 && + color.charCodeAt(1) === 120 /* '0x' */ + ) { + hex = color.slice(2); + } else { + hex = color.length === 6 ? color + 'ff' : color; } - return Number('0x' + (color.length === 6 ? color + 'ff' : color)); + return parseInt(hex, 16); } return 0x00000000;