FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

fix(core): resolve gradient stop positions the way CSS does · NativeScript/NativeScript@6c80db3 · GitHub

Commit 6c80db3

Browse files
committed
fix(core): resolve gradient stop positions the way CSS does
A linear-gradient stop without a position was handed to the platform as missing. Android's float[] left it at 0, so `#fff 68%, #f4f4f5` rendered as a hard edge at 68% instead of a ramp to the end; iOS passed a shorter locations array than colors and let CAGradientLayer guess. Resolve positions once, per CSS: a first stop without a position sits at 0, a last one at 1, runs of unpositioned stops spread evenly between their positioned neighbours, and a position lower than one before it is raised to that value. Both platforms now receive a complete, monotonic list.
1 parent 6dba52f commit 6c80db3

4 files changed

Lines changed: 86 additions & 19 deletions

File tree

‎packages/core/ui/styling/background.android.ts‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { View } from '../core/view';
2-
import { LinearGradient } from './linear-gradient';
2+
import { LinearGradient, resolveGradientStopOffsets } from './linear-gradient';
33
import { ClipPathFunction } from './clip-path-function';
44
import { isDataURI, isFileOrResourcePath, RESOURCE_PREFIX, FILE_PREFIX } from '../../utils';
55
import { CSSValue, parse } from '../../css-value/reworkcss-value';
@@ -15,13 +15,10 @@ function fromBase64(source: string): android.graphics.Bitmap {
1515
function fromGradient(gradient: LinearGradient): org.nativescript.widgets.LinearGradientDefinition {
1616
const colors = Array.create('int', gradient.colorStops.length);
1717
const stops = Array.create('float', gradient.colorStops.length);
18-
let hasStops = false;
18+
const offsets = resolveGradientStopOffsets(gradient.colorStops);
1919
gradient.colorStops.forEach((stop, index) => {
2020
colors[index] = stop.color.android;
21-
if (stop.offset) {
22-
stops[index] = stop.offset.value;
23-
hasStops = true;
24-
}
21+
stops[index] = offsets[index];
2522
});
2623

2724
const alpha = gradient.angle / (Math.PI * 2);
@@ -30,7 +27,7 @@ function fromGradient(gradient: LinearGradient): org.nativescript.widgets.Linear
3027
const endX = Math.pow(Math.sin(Math.PI * (alpha + 0.25)), 2);
3128
const endY = Math.pow(Math.sin(Math.PI * alpha), 2);
3229

33-
return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, hasStops ? stops : null);
30+
return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, stops);
3431
}
3532

3633
const pattern = /url\(('|")(.*?)\1\)/;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { resolveGradientStopOffsets } from './linear-gradient';
2+
3+
function stops(...offsets: (number | undefined)[]) {
4+
return offsets.map((value) => (value === undefined ? { color: null } : { color: null, offset: { unit: '%' as const, value } }));
5+
}
6+
7+
describe('resolveGradientStopOffsets', () => {
8+
it('fills a missing final stop with 1', () => {
9+
expect(resolveGradientStopOffsets(stops(0.68, undefined))).toEqual([0.68, 1]);
10+
});
11+
12+
it('spreads unpositioned stops evenly', () => {
13+
expect(resolveGradientStopOffsets(stops(undefined, undefined, undefined))).toEqual([0, 0.5, 1]);
14+
const [a, b, c] = resolveGradientStopOffsets(stops(0.3, undefined, 0.6));
15+
expect([a, c]).toEqual([0.3, 0.6]);
16+
expect(b).toBeCloseTo(0.45, 10);
17+
});
18+
19+
it('spreads a run between its positioned neighbours', () => {
20+
const [a, b, c, d, e] = resolveGradientStopOffsets(stops(undefined, 0.2, undefined, undefined, 1));
21+
expect([a, b, e]).toEqual([0, 0.2, 1]);
22+
expect(c).toBeCloseTo(0.4667, 3);
23+
expect(d).toBeCloseTo(0.7333, 3);
24+
});
25+
26+
it('raises a position that goes backwards', () => {
27+
expect(resolveGradientStopOffsets(stops(0.5, 0.2))).toEqual([0.5, 0.5]);
28+
});
29+
30+
it('keeps fully positioned stops as written', () => {
31+
expect(resolveGradientStopOffsets(stops(0, 0.52, 0.84, 1))).toEqual([0, 0.52, 0.84, 1]);
32+
});
33+
34+
it('handles no stops', () => {
35+
expect(resolveGradientStopOffsets([])).toEqual([]);
36+
});
37+
});

‎packages/core/ui/styling/linear-gradient.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,46 @@ export interface ColorStop {
77
offset?: CoreTypes.LengthPercentUnit;
88
}
99

10+
/**
11+
* Positions for every color stop, per CSS: a first stop without a position
12+
* sits at 0, a last one at 1, a run of unpositioned stops is spread evenly
13+
* between its positioned neighbours, and a position lower than one before it
14+
* is raised to that value.
15+
*/
16+
export function resolveGradientStopOffsets(colorStops: readonly ColorStop[]): number[] {
17+
const count = colorStops.length;
18+
if (count === 0) {
19+
return [];
20+
}
21+
const offsets: (number | undefined)[] = colorStops.map((stop) => (stop.offset ? stop.offset.value : undefined));
22+
if (offsets[0] === undefined) {
23+
offsets[0] = 0;
24+
}
25+
if (offsets[count - 1] === undefined) {
26+
offsets[count - 1] = 1;
27+
}
28+
let highest = offsets[0];
29+
for (let i = 1; i < count; i++) {
30+
const offset = offsets[i];
31+
if (offset !== undefined) {
32+
offsets[i] = Math.max(offset, highest);
33+
highest = offsets[i];
34+
}
35+
}
36+
let start = 0;
37+
for (let i = 1; i < count; i++) {
38+
if (offsets[i] === undefined) {
39+
continue;
40+
}
41+
const gap = i - start;
42+
for (let k = start + 1; k < i; k++) {
43+
offsets[k] = offsets[start] + ((offsets[i] - offsets[start]) * (k - start)) / gap;
44+
}
45+
start = i;
46+
}
47+
return offsets as number[];
48+
}
49+
1050
export class LinearGradient {
1151
public angle: number;
1252
public colorStops: ColorStop[];

‎packages/core/ui/utils.ios.ts‎

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Screen } from '../platform';
22
import * as utils from '../utils';
3-
import { LinearGradient } from './styling/linear-gradient';
3+
import { LinearGradient, resolveGradientStopOffsets } from './styling/linear-gradient';
44
import { NativeScriptUIView } from './utils';
55

66
export namespace ios {
@@ -52,21 +52,14 @@ export namespace ios {
5252

5353
const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length);
5454
const iosStops = NSMutableArray.alloc<number>().initWithCapacity(gradient.colorStops.length);
55-
let hasStops = false;
56-
57-
gradient.colorStops.forEach((stop) => {
55+
const offsets = resolveGradientStopOffsets(gradient.colorStops);
56+
gradient.colorStops.forEach((stop, index) => {
5857
iosColors.addObject(stop.color.ios.CGColor);
59-
if (stop.offset) {
60-
iosStops.addObject(stop.offset.value);
61-
hasStops = true;
62-
}
58+
iosStops.addObject(offsets[index]);
6359
});
6460

6561
gradientLayer.colors = iosColors;
66-
67-
if (hasStops) {
68-
gradientLayer.locations = iosStops;
69-
}
62+
gradientLayer.locations = iosStops;
7063

7164
const alpha = gradient.angle / (Math.PI * 2);
7265
const startX = Math.pow(Math.sin(Math.PI * (alpha + 0.75)), 2);

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL