blob: a7f5b16923eaf5d8a8b250e5f0dd00744bf5890e [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import 'shader_builder.dart';
import 'webgl_context.dart';
/// Converts colors and stops to typed array of bias, scale and threshold to use
/// in shaders.
///
/// A color is generated by taking a t value [0..1] and computing
/// t * scale + bias.
///
/// Example: For stops 0.0 t1, t2, 1.0 and colors c0, c1, c2, c3
/// Given t1<t<t2 outputColor = t * scale + bias.
/// = c1 + (t - t1)/(t2 - t1) * (c2 - c1)
/// = t * (c2 - c1)/(t2 - t1) + c1 - t1/(t2 - t1) * (c2 - c1)
/// scale = (c2 - c1) / (t2 - t1)
/// bias = c1 - t1 / (t2 - t1) * (c2 - c1)
class NormalizedGradient {
final Float32List _thresholds;
final Float32List _bias;
final Float32List _scale;
final int thresholdCount;
factory NormalizedGradient(List<ui.Color> colors, {List<double>? stops}) {
// If colorStops is not provided, then only two stops, at 0.0 and 1.0,
// are implied (and colors must therefore only have two entries).
assert(stops != null || colors.length == 2);
stops ??= const <double>[0.0, 1.0];
final int colorCount = colors.length;
int normalizedCount = colorCount;
final bool addFirst = stops[0] != 0.0;
final bool addLast = stops.last != 1.0;
if (addFirst) {
normalizedCount++;
}
if (addLast) {
normalizedCount++;
}
final Float32List bias = Float32List(normalizedCount * 4);
final Float32List scale = Float32List(normalizedCount * 4);
final Float32List thresholds =
Float32List(4 * ((normalizedCount - 1) ~/ 4 + 1));
int targetIndex = 0;
int thresholdIndex = 0;
if (addFirst) {
final ui.Color c = colors[0];
bias[targetIndex++] = c.red / 255.0;
bias[targetIndex++] = c.green / 255.0;
bias[targetIndex++] = c.blue / 255.0;
bias[targetIndex++] = c.alpha / 255.0;
thresholds[thresholdIndex++] = 0.0;
}
for (final ui.Color c in colors) {
bias[targetIndex++] = c.red / 255.0;
bias[targetIndex++] = c.green / 255.0;
bias[targetIndex++] = c.blue / 255.0;
bias[targetIndex++] = c.alpha / 255.0;
}
for (final double stop in stops) {
thresholds[thresholdIndex++] = stop;
}
if (addLast) {
final ui.Color c = colors.last;
bias[targetIndex++] = c.red / 255.0;
bias[targetIndex++] = c.green / 255.0;
bias[targetIndex++] = c.blue / 255.0;
bias[targetIndex++] = c.alpha / 255.0;
thresholds[thresholdIndex++] = 1.0;
}
// Now that we have bias for each color stop, we can compute scale based
// on delta between colors.
final int lastColorIndex = 4 * (normalizedCount - 1);
for (int i = 0; i < lastColorIndex; i++) {
final int thresholdIndex = i >> 2;
scale[i] = (bias[i + 4] - bias[i]) /
(thresholds[thresholdIndex + 1] - thresholds[thresholdIndex]);
}
scale[lastColorIndex] = 0.0;
scale[lastColorIndex + 1] = 0.0;
scale[lastColorIndex + 2] = 0.0;
scale[lastColorIndex + 3] = 0.0;
// Compute bias = colorAtStop - stopValue * (scale).
for (int i = 0; i < normalizedCount; i++) {
final double t = thresholds[i];
final int colorIndex = i * 4;
bias[colorIndex] -= t * scale[colorIndex];
bias[colorIndex + 1] -= t * scale[colorIndex + 1];
bias[colorIndex + 2] -= t * scale[colorIndex + 2];
bias[colorIndex + 3] -= t * scale[colorIndex + 3];
}
return NormalizedGradient._(normalizedCount, thresholds, scale, bias);
}
NormalizedGradient._(
this.thresholdCount, this._thresholds, this._scale, this._bias);
/// Sets uniforms for threshold, bias and scale for program.
void setupUniforms(GlContext gl, GlProgram glProgram) {
for (int i = 0; i < thresholdCount; i++) {
final Object biasId = gl.getUniformLocation(glProgram.program, 'bias_$i');
gl.setUniform4f(biasId, _bias[i * 4], _bias[i * 4 + 1], _bias[i * 4 + 2],
_bias[i * 4 + 3]);
final Object scaleId = gl.getUniformLocation(glProgram.program, 'scale_$i');
gl.setUniform4f(scaleId, _scale[i * 4], _scale[i * 4 + 1],
_scale[i * 4 + 2], _scale[i * 4 + 3]);
}
for (int i = 0; i < _thresholds.length; i += 4) {
final Object thresId =
gl.getUniformLocation(glProgram.program, 'threshold_${i ~/ 4}');
gl.setUniform4f(thresId, _thresholds[i], _thresholds[i + 1],
_thresholds[i + 2], _thresholds[i + 3]);
}
}
/// Returns bias component at index.
double biasAt(int index) => _bias[index];
/// Returns scale component at index.
double scaleAt(int index) => _scale[index];
/// Returns threshold at index.
double thresholdAt(int index) => _thresholds[index];
}
/// Writes fragment shader code to search for probe value in source data and set
/// bias and scale to be used for computation.
///
/// Source data for thresholds is provided using ceil(count/4) packed vec4
/// uniforms.
///
/// Bias and scale data are vec4 uniforms that hold color data.
void writeUnrolledBinarySearch(ShaderMethod method, int start, int end,
{required String probe,
required String sourcePrefix,
required String biasName,
required String scaleName}) {
if (start == end) {
final String biasSource = '${biasName}_$start';
method.addStatement('$biasName = $biasSource;');
final String scaleSource = '${scaleName}_$start';
method.addStatement('$scaleName = $scaleSource;');
} else {
// Add probe check.
final int mid = (start + end) ~/ 2;
String thresholdAtMid = '${sourcePrefix}_${(mid + 1) ~/ 4}';
thresholdAtMid += '.${vectorComponentIndexToName((mid + 1) % 4)}';
method.addStatement('if ($probe < $thresholdAtMid) {');
method.indent();
writeUnrolledBinarySearch(method, start, mid,
probe: probe,
sourcePrefix: sourcePrefix,
biasName: biasName,
scaleName: scaleName);
method.unindent();
method.addStatement('} else {');
method.indent();
writeUnrolledBinarySearch(method, mid + 1, end,
probe: probe,
sourcePrefix: sourcePrefix,
biasName: biasName,
scaleName: scaleName);
method.unindent();
method.addStatement('}');
}
}
String vectorComponentIndexToName(int index) {
assert(index >= 0 && index <= 4);
return 'xyzw'[index];
}