A Pen by Alex Riviere on CodePen.
Created
March 7, 2024 12:53
-
-
Save meodai/a7f5b0c3fe88d2890b81e6b76a28583e to your computer and use it in GitHub Desktop.
Scale Spread Array and generator function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="array">Original</div> | |
<div class="generator">Generator</div> | |
<div class="optimizedArray">Combo</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
console.clear(); | |
/** | |
* Linearly interpolates between two values. | |
* | |
* @param {number} amt - The interpolation amount (usually between 0 and 1). | |
* @param {number} from - The starting value. | |
* @param {number} to - The ending value. | |
* @returns {number} - The interpolated value. | |
*/ | |
const lerp = (amt, from, to) => from + amt * (to - from); | |
/** | |
* @callback FillFunction<T> | |
* @param {number} percent - The percentage between the current and next step | |
* @param {T} from - the previous step value | |
* @param {T} to - the next step value | |
* @returns {T} - the interpolated value | |
*/ | |
/** | |
* Scales and spreads an array to the target size as a generator. | |
* | |
* @param {unknown[]} initial - The initial array of values. | |
* @param {number} targetSize - The desired size of the number of steps. | |
* @param {FillFunction} fillFunction - The interpolation function (default is lerp). | |
* @yeilds {unknown} The scaled and spread value for the current step. | |
* @throws {Error} If the initial array is empty or target size is invalid. | |
*/ | |
function* scaleSpread(initial, targetSize, fillFunction = lerp){ | |
if (initial.length < 2) { | |
throw new Error("Initial array must have minimum length of 2."); | |
} | |
if (targetSize < initial.length) { | |
throw new Error("Target size must be greater than or equal to the initial array length."); | |
} | |
// We need to track where our original values will end up | |
const originalValues = {} | |
// How much space is between each original entry as a decimal | |
const spaceBetween = (targetSize - 1)/(initial.length-1); | |
// loop over the remaining values | |
for(let i=0; i < initial.length; i++){ | |
const key = Math.round(spaceBetween*i); | |
const next = Math.round(spaceBetween*(i+1)); | |
//console.log(key, next); | |
const result = { | |
key, | |
value:initial[i], | |
next, | |
} | |
if(next > targetSize-1){ | |
result.next = key; | |
} | |
// round the product of the index * the space between for more even distribution | |
originalValues[key]=result; | |
} | |
// Store our current and next values outside the loop for quicker lookup | |
let current = originalValues[0]; | |
let next = originalValues[current.next]; | |
// we will yeild in here | |
for(let i = 0; i<targetSize; i++){ | |
// don't update current and next until we get to their index number | |
if(next?.key && i >= next?.key){ | |
current = next; | |
next = originalValues[current.next]; | |
} | |
if(next){ | |
let percentage = (i - current.key)/(next.key-current.key) | |
if(Number.isNaN(percentage)) percentage = 1; | |
//console.log(`i:${i}, key:${key}, current.value:${current.value}, next.value:${next?.value}`); | |
yield fillFunction(percentage, current.value, next?.value) | |
} | |
} | |
} | |
/** | |
* Scales and spreads an array to the target size using interpolation. | |
* | |
* @param {Array} initial - The initial array of values. | |
* @param {number} targetSize - The desired size of the resulting array. | |
* @param {function} fillFunction - The interpolation function (default is lerp). | |
* @returns {Array} The scaled and spread array. | |
* @throws {Error} If the initial array is empty or target size is invalid. | |
*/ | |
const scaleSpreadArray = (initial, targetSize, fillFunction = lerp) => { | |
if (initial.length === 0) { | |
throw new Error("Initial array must not be empty."); | |
} | |
if (targetSize < initial.length) { | |
throw new Error("Target size must be greater than or equal to the initial array length."); | |
} | |
const valuesToAdd = targetSize - initial.length; | |
const chunkArray = initial.map((value) => [value]); | |
for (let i = 0; i < valuesToAdd; i++) { | |
chunkArray[i % (initial.length - 1)].push(null); | |
} | |
for (let i = 0; i < chunkArray.length - 1; i++) { | |
const currentChunk = chunkArray[i]; | |
const nextChunk = chunkArray[i + 1]; | |
const currentValue = currentChunk[0]; | |
const nextValue = nextChunk[0]; | |
for (let j = 1; j < currentChunk.length; j++) { | |
const percent = j / currentChunk.length; | |
currentChunk[j] = fillFunction(percent, currentValue, nextValue); | |
} | |
} | |
return chunkArray.flat(); | |
}; | |
/** | |
* @callback FillFunction<T> | |
* @param {number} percent - The percentage between the current and next step | |
* @param {T} from - the previous step value | |
* @param {T} to - the next step value | |
* @returns {T} - the interpolated value | |
*/ | |
/** | |
* Scales and spreads an array to the target size as a generator. | |
* | |
* @param {unknown[]} initial - The initial array of values. | |
* @param {number} targetSize - The desired size of the number of steps. | |
* @param {FillFunction} fillFunction - The interpolation function (default is lerp). | |
* @yeilds {unknown} The scaled and spread value for the current step. | |
* @throws {Error} If the initial array is empty or target size is invalid. | |
*/ | |
function scaleSpreadArrayAlex(initial, targetSize, fillFunction = lerp){ | |
if (initial.length < 2) { | |
throw new Error("Initial array must have minimum length of 2."); | |
} | |
if (targetSize < initial.length) { | |
throw new Error("Target size must be greater than or equal to the initial array length."); | |
} | |
// We need to track where our original values will end up | |
const originalValues = {} | |
// How much space is between each original entry as a decimal | |
const spaceBetween = (targetSize - 1)/(initial.length-1); | |
// loop over the remaining values | |
for(let i=0; i < initial.length; i++){ | |
const key = Math.round(spaceBetween*i); | |
const next = Math.round(spaceBetween*(i+1)); | |
//console.log(key, next); | |
const result = { | |
key, | |
value:initial[i], | |
next, | |
} | |
if(next > targetSize-1){ | |
result.next = key; | |
} | |
// round the product of the index * the space between for more even distribution | |
originalValues[key]=result; | |
} | |
// Store our current and next values outside the loop for quicker lookup | |
let current = originalValues[0]; | |
let next = originalValues[current.next]; | |
const result = []; | |
// we will yeild in here | |
for(let i = 0; i<targetSize; i++){ | |
// don't update current and next until we get to their index number | |
if(next?.key && i >= next?.key){ | |
current = next; | |
next = originalValues[current.next]; | |
} | |
if(next){ | |
let percentage = (i - current.key)/(next.key-current.key) | |
if(Number.isNaN(percentage)) percentage = 1; | |
//console.log(`i:${i}, key:${key}, current.value:${current.value}, next.value:${next?.value}`); | |
result.push(fillFunction(percentage, current.value, next?.value)); | |
} | |
} | |
return result; | |
} | |
function doIt () { | |
let h = Math.random() * 360; | |
let l = 80 + Math.random() * 20; | |
let c = Math.random() * 20; | |
const startColors = [ | |
`lch(${l}% ${c}% ${h})`, | |
`lch(${l -= Math.random() * 40}% ${c += Math.random() * 40}% ${h += (Math.random() * 360) % 360})`, | |
`lch(${l -= Math.random() * 40}% ${c += Math.random() * 40}% ${h += (Math.random() * 360) % 360})`, | |
]; | |
//const size = 5 + ~~(Math.random() * 1000) | |
const size = 5000; | |
const interpolate = (percent, lastValue, nextValue) => { | |
//console.log(percent, lastValue, nextValue, `color-mix(in lab, ${nextValue} ${(percent * 100).toFixed(2)}%, ${lastValue})`) | |
return `color-mix(in lab, ${nextValue} ${(percent * 100).toFixed(2)}%, ${lastValue})` | |
} | |
console.time('scaleSpread'); | |
const generatorColors = [...scaleSpread(startColors,size,interpolate)] | |
console.timeEnd('scaleSpread'); | |
console.time('scaleSpreadArray'); | |
const arrayColors = scaleSpreadArray(startColors,size,interpolate); | |
console.timeEnd('scaleSpreadArray'); | |
console.time('scaleSpreadArrayAlex'); | |
const arrayAlexColors = scaleSpreadArrayAlex(startColors,size,interpolate); | |
console.timeEnd('scaleSpreadArrayAlex'); | |
document.documentElement.style.setProperty( | |
'--gs-array', arrayColors.map( | |
(c, i) => `${c} ${i/arrayColors.length*100}% ${(i+1)/arrayColors.length*100}%` | |
).join(',') | |
); | |
document.documentElement.style.setProperty( | |
'--gs-generator', generatorColors.map( | |
(c, i) => `${c} ${i/generatorColors.length*100}% ${(i+1)/generatorColors.length*100}%` | |
).join(',') | |
); | |
document.documentElement.style.setProperty( | |
'--gs-optimizedarray', generatorColors.map( | |
(c, i) => `${c} ${i/generatorColors.length*100}% ${(i+1)/generatorColors.length*100}%` | |
).join(',') | |
); | |
const initalInts = [ | |
~~(Math.random() * 10), | |
~~(Math.random() * 10), | |
~~(Math.random() * 10), | |
] | |
console.log( | |
`example using random ints between 0 and 10`, | |
initalInts, | |
scaleSpreadArray( | |
initalInts, | |
10 | |
)) | |
console.log( | |
`example using random ints between 0 and 10`, | |
initalInts, | |
[...scaleSpread( | |
initalInts, | |
10 | |
)]) | |
} | |
doIt(); | |
document.documentElement.addEventListener('click', doIt); | |
//console.log(scaleSpreadArray([1,2,3], 5, ()=>{})) | |
// console.log(scaleSpreadArray([1], 2)) // this will error out | |
//console.log(...scaleSpread([1,2,3], 4)) | |
//console.log(...scaleSpread([1,2,3,4,5,6,7], 17)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
html { | |
height: 100%; | |
} | |
body { | |
display:grid; | |
grid-template-columns: 1fr 1fr 1fr; | |
min-height: 100vh; | |
margin: 10px; | |
gap:10px; | |
} | |
body>*{ | |
height: 100%; | |
} | |
.array{ | |
background: linear-gradient(to bottom, var(--gs-array)); | |
} | |
.generator{ | |
background: linear-gradient(to bottom, var(--gs-generator)); | |
} | |
.optimizedArray{ | |
background: linear-gradient(to bottom, var(--gs-optimizedarray)); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment