summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/html/canvas/element/manual/shadows/shadowBlur_gaussian_tolerance.1.html
blob: eec27bf108e0f625ae89240917855007e1fb9c66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
<!DOCTYPE HTML>
<title>Test of canvas shadowBlur Gaussian blur pixel values</title>
<meta charset=UTF-8>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<body>
<h1>Test of canvas shadowBlur Gaussian blur pixel values</h1>
<script>

/**
 * See https://en.wikipedia.org/wiki/Error_function#Approximation_with_elementary_functions
 */
function erf(x) {
  if (x < 0) {
    return -erf(-x);
  }
  var p = 0.3275911, a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741, a4 = -1.453152027, a5 = 1.061405429;
  var t = 1 / (1 + p * x);
  return 1 - Math.exp(-x * x) * t * (a1 + t * (a2 + t * (a3 + t * (a4 + t * a5))));
}

/**
 * See https://en.wikipedia.org/wiki/Normal_distribution#Cumulative_distribution_function
 * and https://en.wikipedia.org/wiki/Normal_distribution#Numerical_approximations_for_the_normal_CDF
 */
function standard_normal_distribution_cumulative(x) {
  return 0.5 * (1 + erf(x / Math.SQRT2));
}

/**
 * Verify a single pixel; helper for run_blur_test.
 *   params - same as run_blur_test
 *   row & col - relative to the corner of the rectangle being blurred
 *   actual - actual color found there on the canvas
 */
function test_pixel(params, row, col, shadowOffset, actual) {
  var expected_gaussian;
  if (params.expected_sigma > 0) {
    // Compute positions within a standard normal distribution (i.e.,
    // where mean (μ) is and standard deviation (σ) is 1) in both
    // dimensions.
    // Add 0.5 because we want the middle of the pixel rather than the edge.
    var pos_x = (col - shadowOffset + 0.5) / params.expected_sigma;
    var pos_y = (row - shadowOffset + 0.5) / params.expected_sigma;

    // Find the expected color value based on a Gaussian blur function.
    // Since we're blurring the corner of a "very large" rectangle, we
    // can, instead of sampling all of the pixels, use the cumulative
    // form of the normal (Gaussian) distribution and pass it the
    // position of the color transition (the edges of the rectangle),
    // since we know everything above and to the left of that position
    // is one color, and everything that is either below or to the right
    // of that position is another color.
    //
    // NOTE: This assumes color-interpolation happens in sRGB rather
    // than linearRGB.  The canvas spec doesn't appear to be clear on
    // this point.  If it were linearRGB, we'd need to apply the
    // correction after doing this calculation.  (No correction to the
    // input is needed since the input is all 0 or 1.)
    expected_gaussian = standard_normal_distribution_cumulative(-pos_x) *
                        standard_normal_distribution_cumulative(-pos_y);
  } else {
    if (col >= shadowOffset || row >= shadowOffset) {
      expected_gaussian = 0;
    } else {
      expected_gaussian = 1;
    }
  }
  // TODO: maybe also compute expected value by triple box blur?

  /*
   * https://html.spec.whatwg.org/multipage/canvas.html#when-shadows-are-drawn
   * describes how to draw shadows in canvas.  It says, among other things:
   *
   *   Perform a 2D Gaussian Blur on B, using σ as the standard deviation.
   *
   * without giving *any* allowance for error.
   *
   * However, other specifications that require Gaussian blurs allow some
   * error; https://www.w3.org/TR/css-backgrounds-3/#shadow-blur allows up to
   * 5%, and https://drafts.fxtf.org/filter-effects/#feGaussianBlurElement
   * allows use of a triple box blur which is within 3%.
   *
   * Since expecting zero error is unreasonable, this test tests for the least
   * restrictive of these bounds, the 5% error.
   *
   * Note that this allows 5% error in the color component, but there's no
   * tolerance for error in the position; see comment below about sizes.
   */

  // Allow any rounding direction.
  var min_b = Math.max(  0, Math.floor((expected_gaussian - 0.05) * 255));
  var max_b = Math.min(255, Math.ceil ((expected_gaussian + 0.05) * 255));
  var min_r = 255 - max_b;
  var max_r = 255 - min_b;

  var pos = "at row " + row + " col " + col + " ";

  assert_true(min_r <= actual.r && actual.r <= max_r,
              pos + "red component " + actual.r + " should be between " +
              min_r + " and " + max_r + " (inclusive).");
  assert_true(min_b <= actual.b && actual.b <= max_b,
              pos + "blue component " + actual.b + " should be between " +
              min_b + " and " + max_b + " (inclusive).");
  assert_equals(actual.g, 0, pos + "green component should be 0");
  assert_equals(actual.a, 255, pos + "alpha component should be 255");
}

/**
 * Run a test of a single shadowBlur drawing operation.  Expects a
 * parameters object containing:
 *   name - name of test
 *   canvas_width - width of canvas to create
 *   canvas_height - height of canvas to create
 *   shadowBlur - shadowBlur to use for the test drawing operation
 *   expected_sigma - the standard deviation of the gaussian function
 *     that shadowBlur is expected to produce
 *   pixel_skip - how many pixels to skip when sampling results.  Should
 *     be relatively prime with canvas_width.
 */
function run_blur_test(params) {
  test(function() {
    var canvas = document.createElement("canvas");
    canvas.setAttribute("width", params.canvas_width);
    canvas.setAttribute("height", params.canvas_height);
    document.body.appendChild(canvas);
    var cx = canvas.getContext("2d");

    cx.fillStyle = "red";
    cx.fillRect(0, 0, params.canvas_width, params.canvas_height);

    // Fill a huge rect just to the top and left of the canvas, with its shadow
    // blur centered at the middle of the canvas.
    let edge = Math.floor(params.canvas_width / 2); // position of vertical
    let big = Math.max(Math.ceil(params.expected_sigma * 1000),
                       params.canvas_width,
                       params.canvas_height);
    cx.shadowBlur = params.shadowBlur;
    cx.fillStyle = "green";
    cx.shadowColor = "blue";
    cx.shadowOffsetX = edge;
    cx.shadowOffsetY = edge;
    cx.fillRect(-big, -big, big, big);

    var imageData =
      cx.getImageData(0, 0, params.canvas_width, params.canvas_height);
    for (var i = 0, i_max = params.canvas_width * params.canvas_height;
         i < i_max;
         i += params.pixel_skip) {
      var row = Math.floor(i / params.canvas_width);
      var col = i - row * params.canvas_width;

      var actual = { r: imageData.data[i * 4],
                     g: imageData.data[i * 4 + 1],
                     b: imageData.data[i * 4 + 2],
                     a: imageData.data[i * 4 + 3] };

      test_pixel(params, row, col, edge, actual);
    }
  }, "shadowBlur Gaussian pixel values for " + params.name);
}

run_blur_test({
  name: "no blur",
  canvas_width: 4,
  canvas_height: 4,
  shadowBlur: 0,
  expected_sigma: 0,
  pixel_skip: 1
});
run_blur_test({
  name: "small blur",
  canvas_width: 20,
  canvas_height: 20,
  // Try to test something smaller than 8 due to historic change in
  // https://www.w3.org/Bugs/Public/show_bug.cgi?id=10647 , but not too
  // small, to avoid the error from rounding to individual pixels worth
  // of box blur.
  shadowBlur: 6,
  expected_sigma: 3,
  pixel_skip: 3
});
run_blur_test({
  name: "large blur",
  canvas_width: 100,
  canvas_height: 100,
  shadowBlur: 30,
  expected_sigma: 15,
  pixel_skip: 13
});
</script>