feat(non linear font scaling)!: add FontScaleConverter for non-linear font scaling.

Non-linear font scaling is meant to improve readability at larger font
scales: larger fonts will scale up more slowly than smaller fonts, so we
don't get ridiculously huge fonts that don't fit on the screen.

The thinking here is that large fonts are already big enough to read,
but we still want to scale them slightly to preserve the visual
hierarchy when compared to smaller fonts.

The FontScaleConverter converts SP dimensions to DP, to be used in
TypedValue.applyDimension() which will affect most TextViews and font
sizes automatically.

For now, all the lookup tables are hardcoded. A follow-up CL will make
them configurable via XML. The hardcoded arrays also default to a linear
curve, as to not break any tests. The non-linear curve will come in a
follow-up CL, to make it easier to roll-back if requested.

Also included is some Javascript to generate the hardcoded arrays, which
can later be manually tweaked and optimized.

Test: unit, CTS, and manual:
1. Run `adb shell settings put system font_scale 2.0`
2. Check different apps to see if they are readable.

Bug: b/237558231

Change-Id: I17d67252bf31f55e57e1f3e8a0f638770e6d2cfd
This commit is contained in:
Tyler Freeman
2022-11-29 12:09:02 -08:00
parent eb803fd53e
commit 5f7036df71
11 changed files with 682 additions and 6 deletions

View File

@@ -563,6 +563,9 @@ public class CompatibilityInfo implements Parcelable {
if (applyToSize) {
inoutDm.widthPixels = (int) (inoutDm.widthPixels * invertedRatio + 0.5f);
inoutDm.heightPixels = (int) (inoutDm.heightPixels * invertedRatio + 0.5f);
float fontScale = inoutDm.scaledDensity / inoutDm.density;
inoutDm.fontScaleConverter = FontScaleConverterFactory.forScale(fontScale);
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.res;
import android.annotation.NonNull;
import android.util.MathUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Arrays;
/**
* A lookup table for non-linear font scaling. Converts font sizes given in "sp" dimensions to a
* "dp" dimension according to a non-linear curve.
*
* <p>This is meant to improve readability at larger font scales: larger fonts will scale up more
* slowly than smaller fonts, so we don't get ridiculously huge fonts that don't fit on the screen.
*
* <p>The thinking here is that large fonts are already big enough to read, but we still want to
* scale them slightly to preserve the visual hierarchy when compared to smaller fonts.
*
* @hide
*/
public class FontScaleConverter {
/**
* How close the given SP should be to a canonical SP in the array before they are considered
* the same for lookup purposes.
*/
private static final float THRESHOLD_FOR_MATCHING_SP = 0.02f;
@VisibleForTesting
final float[] mFromSpValues;
@VisibleForTesting
final float[] mToDpValues;
/**
* Creates a lookup table for the given conversions.
*
* <p>Any "sp" value not in the lookup table will be derived via linear interpolation.
*
* <p>The arrays must be sorted ascending and monotonically increasing.
*
* @param fromSp array of dimensions in SP
* @param toDp array of dimensions in DP that correspond to an SP value in fromSp
*
* @throws IllegalArgumentException if the array lengths don't match or are empty
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public FontScaleConverter(@NonNull float[] fromSp, @NonNull float[] toDp) {
if (fromSp.length != toDp.length || fromSp.length == 0) {
throw new IllegalArgumentException("Array lengths must match and be nonzero");
}
mFromSpValues = fromSp;
mToDpValues = toDp;
}
/**
* Convert a dimension in "sp" to "dp" using the lookup table.
*
* @hide
*/
public float convertSpToDp(float sp) {
final float spPositive = Math.abs(sp);
// TODO(b/247861374): find a match at a higher index?
final int spRounded = Math.round(spPositive);
final float sign = Math.signum(sp);
final int index = Arrays.binarySearch(mFromSpValues, spRounded);
if (index >= 0 && Math.abs(spRounded - spPositive) < THRESHOLD_FOR_MATCHING_SP) {
// exact match, return the matching dp
return sign * mToDpValues[index];
} else {
// must be a value in between index and index + 1: interpolate.
final int lowerIndex = -(index + 1) - 1;
final float startSp;
final float endSp;
final float startDp;
final float endDp;
if (lowerIndex >= mFromSpValues.length - 1) {
// It's past our lookup table. Determine the last elements' scaling factor and use.
startSp = mFromSpValues[mFromSpValues.length - 1];
startDp = mToDpValues[mFromSpValues.length - 1];
if (startSp == 0) return 0;
final float scalingFactor = startDp / startSp;
return sp * scalingFactor;
} else if (lowerIndex == -1) {
// It's smaller than the smallest value in our table. Interpolate from 0.
startSp = 0;
startDp = 0;
endSp = mFromSpValues[0];
endDp = mToDpValues[0];
} else {
startSp = mFromSpValues[lowerIndex];
endSp = mFromSpValues[lowerIndex + 1];
startDp = mToDpValues[lowerIndex];
endDp = mToDpValues[lowerIndex + 1];
}
return sign * MathUtils.constrainedMap(startDp, endDp, startSp, endSp, spPositive);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
if (!(o instanceof FontScaleConverter)) return false;
FontScaleConverter that = (FontScaleConverter) o;
return Arrays.equals(mFromSpValues, that.mFromSpValues)
&& Arrays.equals(mToDpValues, that.mToDpValues);
}
@Override
public int hashCode() {
int result = Arrays.hashCode(mFromSpValues);
result = 31 * result + Arrays.hashCode(mToDpValues);
return result;
}
@Override
public String toString() {
return "FontScaleConverter{"
+ "fromSpValues="
+ Arrays.toString(mFromSpValues)
+ ", toDpValues="
+ Arrays.toString(mToDpValues)
+ '}';
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.res;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
/**
* Stores lookup tables for creating {@link FontScaleConverter}s at various scales.
*
* @hide
*/
public class FontScaleConverterFactory {
private static final float SCALE_KEY_MULTIPLIER = 100f;
@VisibleForTesting
static final SparseArray<FontScaleConverter> LOOKUP_TABLES = new SparseArray<>();
static {
// These were generated by frameworks/base/tools/fonts/font-scaling-array-generator.js and
// manually tweaked for optimum readability.
put(
/* scaleKey= */ 1.15f,
new FontScaleConverter(
/* fromSp= */
new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
/* toDp= */
new float[] { 9.2f, 11.5f, 13.8f, 16.1f, 20.7f, 23f, 27.6f, 34.5f, 115})
);
put(
/* scaleKey= */ 1.3f,
new FontScaleConverter(
/* fromSp= */
new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
/* toDp= */
new float[] {10.4f, 13f, 15.6f, 18.2f, 23.4f, 26f, 31.2f, 39f, 130})
);
put(
/* scaleKey= */ 1.5f,
new FontScaleConverter(
/* fromSp= */
new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
/* toDp= */
new float[] { 12f, 15f, 18f, 21f, 27f, 30f, 36f, 45f, 150})
);
put(
/* scaleKey= */ 1.8f,
new FontScaleConverter(
/* fromSp= */
new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
/* toDp= */
new float[] {14.4f, 18f, 21.6f, 25.2f, 32.4f, 36f, 43.2f, 54f, 180})
);
put(
/* scaleKey= */ 2f,
new FontScaleConverter(
/* fromSp= */
new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100},
/* toDp= */
new float[] { 16f, 20f, 24f, 28f, 36f, 40f, 48f, 60f, 200})
);
}
private FontScaleConverterFactory() {}
/**
* Finds a matching FontScaleConverter for the given fontScale factor.
*
* @param fontScale the scale factor, usually from {@link Configuration#fontScale}.
*
* @return a converter for the given scale, or null if non-linear scaling should not be used.
*
* @hide
*/
@Nullable
public static FontScaleConverter forScale(float fontScale) {
if (fontScale <= 1) {
// We don't need non-linear curves for shrinking text or for 100%.
// Also, fontScale==0 should not have a curve either
return null;
}
FontScaleConverter lookupTable = get(fontScale);
// TODO(b/247861716): interpolate between two tables when null
return lookupTable;
}
private static void put(float scaleKey, @NonNull FontScaleConverter fontScaleConverter) {
LOOKUP_TABLES.put((int) (scaleKey * SCALE_KEY_MULTIPLIER), fontScaleConverter);
}
@Nullable
private static FontScaleConverter get(float scaleKey) {
return LOOKUP_TABLES.get((int) (scaleKey * SCALE_KEY_MULTIPLIER));
}
}

View File

@@ -430,6 +430,8 @@ public class ResourcesImpl {
// Protect against an unset fontScale.
mMetrics.scaledDensity = mMetrics.density *
(mConfiguration.fontScale != 0 ? mConfiguration.fontScale : 1.0f);
mMetrics.fontScaleConverter =
FontScaleConverterFactory.forScale(mConfiguration.fontScale);
final int width, height;
if (mMetrics.widthPixels >= mMetrics.heightPixels) {

View File

@@ -18,6 +18,7 @@ package android.util;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.res.FontScaleConverter;
import android.os.SystemProperties;
/**
@@ -265,6 +266,15 @@ public class DisplayMetrics {
* increments at runtime based on a user preference for the font size.
*/
public float scaledDensity;
/**
* If non-null, this will be used to calculate font sizes instead of {@link #scaledDensity}.
*
* @hide
*/
@Nullable
public FontScaleConverter fontScaleConverter;
/**
* The exact physical pixels per inch of the screen in the X dimension.
*/
@@ -342,6 +352,7 @@ public class DisplayMetrics {
noncompatScaledDensity = o.noncompatScaledDensity;
noncompatXdpi = o.noncompatXdpi;
noncompatYdpi = o.noncompatYdpi;
fontScaleConverter = o.fontScaleConverter;
}
public void setToDefaults() {
@@ -359,6 +370,7 @@ public class DisplayMetrics {
noncompatScaledDensity = scaledDensity;
noncompatXdpi = xdpi;
noncompatYdpi = ydpi;
fontScaleConverter = null;
}
@Override

View File

@@ -408,7 +408,14 @@ public class TypedValue {
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
if (metrics.fontScaleConverter != null) {
return applyDimension(
COMPLEX_UNIT_DIP,
metrics.fontScaleConverter.convertSpToDp(value),
metrics);
} else {
return value * metrics.scaledDensity;
}
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:

View File

@@ -43,6 +43,7 @@ android_test {
"mockwebserver",
"guava",
"androidx.core_core",
"androidx.core_core-ktx",
"androidx.test.espresso.core",
"androidx.test.ext.junit",
"androidx.test.runner",

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.res
import androidx.core.util.forEach
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FontScaleConverterFactoryTest {
@Test
fun scale200IsTwiceAtSmallSizes() {
val table = FontScaleConverterFactory.forScale(2F)!!
assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
}
@SmallTest
fun missingLookupTableReturnsNull() {
assertThat(FontScaleConverterFactory.forScale(3F)).isNull()
}
@SmallTest
fun missingLookupTable105ReturnsNull() {
assertThat(FontScaleConverterFactory.forScale(1.05F)).isNull()
}
@SmallTest
fun missingLookupTableNegativeReturnsNull() {
assertThat(FontScaleConverterFactory.forScale(-1F)).isNull()
}
@SmallTest
fun unnecessaryFontScalesReturnsNull() {
assertThat(FontScaleConverterFactory.forScale(0F)).isNull()
assertThat(FontScaleConverterFactory.forScale(1F)).isNull()
assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull()
}
@SmallTest
fun tablesMatchAndAreMonotonicallyIncreasing() {
FontScaleConverterFactory.LOOKUP_TABLES.forEach { _, lookupTable ->
assertThat(lookupTable.mToDpValues).hasLength(lookupTable.mFromSpValues.size)
assertThat(lookupTable.mToDpValues).isNotEmpty()
assertThat(lookupTable.mFromSpValues.asList()).isInStrictOrder()
assertThat(lookupTable.mToDpValues.asList()).isInStrictOrder()
assertThat(lookupTable.mFromSpValues.asList()).containsNoDuplicates()
assertThat(lookupTable.mToDpValues.asList()).containsNoDuplicates()
}
}
companion object {
private const val CONVERSION_TOLERANCE = 0.05f
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.res
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class FontScaleConverterTest {
@Test
fun straightInterpolation() {
val table = createTable(8f to 8f, 10f to 10f, 20f to 20f)
assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1f)
assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(8f)
assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(10f)
assertThat(table.convertSpToDp(30F)).isWithin(CONVERSION_TOLERANCE).of(30f)
assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(20f)
assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(5f)
assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
}
@Test
fun interpolate200Percent() {
val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(2f)
assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(16f)
assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(20f)
assertThat(table.convertSpToDp(30F)).isWithin(CONVERSION_TOLERANCE).of(60f)
assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(40f)
assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(10f)
assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
}
@Test
fun interpolate150Percent() {
val table = createTable(2f to 3f, 10f to 15f, 20f to 30f, 100f to 150f)
assertThat(table.convertSpToDp(2F)).isWithin(CONVERSION_TOLERANCE).of(3f)
assertThat(table.convertSpToDp(1F)).isWithin(CONVERSION_TOLERANCE).of(1.5f)
assertThat(table.convertSpToDp(8F)).isWithin(CONVERSION_TOLERANCE).of(12f)
assertThat(table.convertSpToDp(10F)).isWithin(CONVERSION_TOLERANCE).of(15f)
assertThat(table.convertSpToDp(20F)).isWithin(CONVERSION_TOLERANCE).of(30f)
assertThat(table.convertSpToDp(50F)).isWithin(CONVERSION_TOLERANCE).of(75f)
assertThat(table.convertSpToDp(5F)).isWithin(CONVERSION_TOLERANCE).of(7.5f)
assertThat(table.convertSpToDp(0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
}
@Test
fun pastEndsUsesLastScalingFactor() {
val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
assertThat(table.convertSpToDp(100F)).isWithin(CONVERSION_TOLERANCE).of(200f)
assertThat(table.convertSpToDp(31F)).isWithin(CONVERSION_TOLERANCE).of(62f)
assertThat(table.convertSpToDp(1000F)).isWithin(CONVERSION_TOLERANCE).of(2000f)
assertThat(table.convertSpToDp(2000F)).isWithin(CONVERSION_TOLERANCE).of(4000f)
assertThat(table.convertSpToDp(10000F)).isWithin(CONVERSION_TOLERANCE).of(20000f)
}
@Test
fun negativeSpIsNegativeDp() {
val table = createTable(8f to 16f, 10f to 20f, 30f to 60f)
assertThat(table.convertSpToDp(-1F)).isWithin(CONVERSION_TOLERANCE).of(-2f)
assertThat(table.convertSpToDp(-8F)).isWithin(CONVERSION_TOLERANCE).of(-16f)
assertThat(table.convertSpToDp(-10F)).isWithin(CONVERSION_TOLERANCE).of(-20f)
assertThat(table.convertSpToDp(-30F)).isWithin(CONVERSION_TOLERANCE).of(-60f)
assertThat(table.convertSpToDp(-20F)).isWithin(CONVERSION_TOLERANCE).of(-40f)
assertThat(table.convertSpToDp(-5F)).isWithin(CONVERSION_TOLERANCE).of(-10f)
assertThat(table.convertSpToDp(-0F)).isWithin(CONVERSION_TOLERANCE).of(0f)
}
private fun createTable(vararg pairs: Pair<Float, Float>) =
FontScaleConverter(
pairs.map { it.first }.toFloatArray(),
pairs.map { it.second }.toFloatArray()
)
companion object {
private const val CONVERSION_TOLERANCE = 0.05f
}
}

View File

@@ -16,16 +16,17 @@
package android.util
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SmallTest
import androidx.test.runner.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.mock
import kotlin.math.abs
import kotlin.math.min
import kotlin.math.roundToInt
@RunWith(AndroidJUnit4::class)
class TypedValueTest {
@@ -152,4 +153,19 @@ class TypedValueTest {
val widthPx = TypedValue.complexToDimensionPixelSize(widthDimen, metrics)
assertEquals(widthFloat.roundToInt(), widthPx)
}
}
@SmallTest
@Test
fun testNonLinearFontScaling_nullLookupFallsBackToScaledDensity() {
val metrics: DisplayMetrics = mock(DisplayMetrics::class.java)
val fontScale = 2f
metrics.density = 1f
metrics.scaledDensity = fontScale * metrics.density
metrics.fontScaleConverter = null
assertThat(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10f, metrics))
.isEqualTo(20f)
assertThat(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 50f, metrics))
.isEqualTo(100f)
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
Generates arrays for non-linear font scaling, to be pasted into
frameworks/base/core/java/android/content/res/FontScaleConverterFactory.java
To use:
`node font-scaling-array-generator.js`
or just open a browser, open DevTools, and paste into the Console.
*/
/**
* Modify this to match your packages/apps/Settings/res/arrays.xml#entryvalues_font_size
* array so that all possible scales are generated.
*/
const scales = [1.15, 1.30, 1.5, 1.8, 2];
const commonSpSizes = [8, 10, 12, 14, 18, 20, 24, 30, 100];
/**
* Enum for GENERATION_STYLE which determines how to generate the arrays.
*/
const GenerationStyle = {
/**
* Interpolates between hand-tweaked curves. This is the best option and
* shouldn't require any additional tweaking.
*/
CUSTOM_TWEAKED: 'CUSTOM_TWEAKED',
/**
* Uses a curve equation that is mostly correct, but will need manual tweaking
* at some scales.
*/
CURVE: 'CURVE',
/**
* Uses straight linear multiplication. Good starting point for manual
* tweaking.
*/
LINEAR: 'LINEAR'
}
/**
* Determines how arrays are generated. Must be one of the GenerationStyle
* values.
*/
const GENERATION_STYLE = GenerationStyle.CUSTOM_TWEAKED;
// These are hand-tweaked curves from which we will derive the other
// interstitial curves using linear interpolation, in the case of using
// GenerationStyle.CUSTOM_TWEAKED.
const interpolationTargets = {
1.0: commonSpSizes,
1.5: [12, 15, 18, 22, 24, 26, 28, 30, 100],
2.0: [16, 20, 24, 26, 30, 34, 36, 38, 100]
};
/**
* Interpolate a value with specified extrema, to a new value between new
* extrema.
*
* @param value the current value
* @param inputMin minimum the input value can reach
* @param inputMax maximum the input value can reach
* @param outputMin minimum the output value can reach
* @param outputMax maximum the output value can reach
*/
function map(value, inputMin, inputMax, outputMin, outputMax) {
return outputMin + (outputMax - outputMin) * ((value - inputMin) / (inputMax - inputMin));
}
/***
* Interpolate between values a and b.
*/
function lerp(a, b, fraction) {
return (a * (1.0 - fraction)) + (b * fraction);
}
function generateRatios(scale) {
// Find the best two arrays to interpolate between.
let startTarget, endTarget;
let startTargetScale, endTargetScale;
const targetScales = Object.keys(interpolationTargets).sort();
for (let i = 0; i < targetScales.length - 1; i++) {
const targetScaleKey = targetScales[i];
const targetScale = parseFloat(targetScaleKey, 10);
const startTargetScaleKey = targetScaleKey;
const endTargetScaleKey = targetScales[i + 1];
if (scale < parseFloat(startTargetScaleKey, 10)) {
break;
}
startTargetScale = parseFloat(startTargetScaleKey, 10);
endTargetScale = parseFloat(endTargetScaleKey, 10);
startTarget = interpolationTargets[startTargetScaleKey];
endTarget = interpolationTargets[endTargetScaleKey];
}
const interpolationProgress = map(scale, startTargetScale, endTargetScale, 0, 1);
return commonSpSizes.map((sp, i) => {
const originalSizeDp = sp;
let newSizeDp;
switch (GENERATION_STYLE) {
case GenerationStyle.CUSTOM_TWEAKED:
newSizeDp = lerp(startTarget[i], endTarget[i], interpolationProgress);
break;
case GenerationStyle.CURVE: {
let coeff1;
let coeff2;
if (scale < 1) {
// \left(1.22^{-\left(x+5\right)}+0.5\right)\cdot x
coeff1 = -5;
coeff2 = scale;
} else {
// (1.22^{-\left(x-10\right)}+1\right)\cdot x
coeff1 = map(scale, 1, 2, 2, 8);
coeff2 = 1;
}
newSizeDp = ((Math.pow(1.22, (-(originalSizeDp - coeff1))) + coeff2) * originalSizeDp);
break;
}
case GenerationStyle.LINEAR:
newSizeDp = originalSizeDp * scale;
break;
default:
throw new Error('Invalid GENERATION_STYLE');
}
return {
fromSp: sp,
toDp: newSizeDp
}
});
}
const scaleArrays =
scales
.map(scale => {
const scaleString = (scale * 100).toFixed(0);
return {
scale,
name: `font_size_original_sp_to_scaled_dp_${scaleString}_percent`
}
})
.map(scaleArray => {
const items = generateRatios(scaleArray.scale);
return {
...scaleArray,
items
}
});
function formatDigit(d) {
const twoSignificantDigits = Math.round(d * 100) / 100;
return String(twoSignificantDigits).padStart(4, ' ');
}
console.log(
'' +
scaleArrays.reduce(
(previousScaleArray, currentScaleArray) => {
const itemsFromSp = currentScaleArray.items.map(d => d.fromSp)
.map(formatDigit)
.join('f, ');
const itemsToDp = currentScaleArray.items.map(d => d.toDp)
.map(formatDigit)
.join('f, ');
return previousScaleArray + `
put(
/* scaleKey= */ ${currentScaleArray.scale}f,
new FontScaleConverter(
/* fromSp= */
new float[] {${itemsFromSp}},
/* toDp= */
new float[] {${itemsToDp}})
);
`;
},
''));