Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(351)

Side by Side Diff: experimental/Intersection/hg.htm

Issue 867213004: remove prototype pathops code (Closed) Base URL: https://skia.googlesource.com/skia.git@master
Patch Set: Created 5 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « experimental/Intersection/edge_Prefix.pch ('k') | experimental/Intersection/op.htm » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 <html>
2 <head>
3 <div style="height:0">
4
5 <div id="cubic1">
6 {{3.13,2.74}, {1.08,4.62}, {3.71,0.94}, {2.01,3.81}}
7 {{6.71,3.14}, {7.99,2.75}, {8.27,1.96}, {6.35,3.57}}
8 {{9.45,10.67}, {10.05,5.78}, {13.95,7.46}, {14.72,5.29}}
9 {{3.34,8.98}, {1.95,10.27}, {3.76,7.65}, {4.96,10.64}}
10 </div>
11
12 </div>
13
14 <script type="text/javascript">
15
16 var testDivs = [
17 cubic1,
18 ];
19
20 var scale, columns, rows, xStart, yStart;
21
22 var ticks = 10;
23 var at_x = 13 + 0.5;
24 var at_y = 23 + 0.5;
25 var decimal_places = 3;
26 var tests = [];
27 var testTitles = [];
28 var testIndex = 0;
29 var ctx;
30 var minScale = 1;
31 var subscale = 1;
32 var curveT = -1;
33 var xmin, xmax, ymin, ymax;
34
35 var mouseX, mouseY;
36 var mouseDown = false;
37
38 var draw_deriviatives = false;
39 var draw_endpoints = true;
40 var draw_hodo = false;
41 var draw_hodo2 = false;
42 var draw_hodo_origin = true;
43 var draw_midpoint = false;
44 var draw_tangents = true;
45 var draw_sequence = true;
46
47 function parse(test, title) {
48 var curveStrs = test.split("{{");
49 if (curveStrs.length == 1)
50 curveStrs = test.split("=(");
51 var pattern = /[a-z$=]?-?\d+\.*\d*e?-?\d*/g;
52 var curves = [];
53 for (var c in curveStrs) {
54 var curveStr = curveStrs[c];
55 var points = curveStr.match(pattern);
56 var pts = [];
57 for (var wd in points) {
58 var num = parseFloat(points[wd]);
59 if (isNaN(num)) continue;
60 pts.push(num);
61 }
62 if (pts.length > 2)
63 curves.push(pts);
64 }
65 if (curves.length >= 1) {
66 tests.push(curves);
67 testTitles.push(title);
68 }
69 }
70
71 function init(test) {
72 var canvas = document.getElementById('canvas');
73 if (!canvas.getContext) return;
74 canvas.width = window.innerWidth - 20;
75 canvas.height = window.innerHeight - 20;
76 ctx = canvas.getContext('2d');
77 xmin = Infinity;
78 xmax = -Infinity;
79 ymin = Infinity;
80 ymax = -Infinity;
81 for (var curves in test) {
82 var curve = test[curves];
83 var last = curve.length;
84 for (var idx = 0; idx < last; idx += 2) {
85 xmin = Math.min(xmin, curve[idx]);
86 xmax = Math.max(xmax, curve[idx]);
87 ymin = Math.min(ymin, curve[idx + 1]);
88 ymax = Math.max(ymax, curve[idx + 1]);
89 }
90 }
91 xmin -= 1;
92 var testW = xmax - xmin;
93 var testH = ymax - ymin;
94 subscale = 1;
95 while (testW * subscale < 0.1 && testH * subscale < 0.1) {
96 subscale *= 10;
97 }
98 while (testW * subscale > 10 && testH * subscale > 10) {
99 subscale /= 10;
100 }
101 calcFromScale();
102 }
103
104 function hodograph(cubic) {
105 var hodo = [];
106 hodo[0] = 3 * (cubic[2] - cubic[0]);
107 hodo[1] = 3 * (cubic[3] - cubic[1]);
108 hodo[2] = 3 * (cubic[4] - cubic[2]);
109 hodo[3] = 3 * (cubic[5] - cubic[3]);
110 hodo[4] = 3 * (cubic[6] - cubic[4]);
111 hodo[5] = 3 * (cubic[7] - cubic[5]);
112 return hodo;
113 }
114
115 function hodograph2(cubic) {
116 var quad = hodograph(cubic);
117 var hodo = [];
118 hodo[0] = 2 * (quad[2] - quad[0]);
119 hodo[1] = 2 * (quad[3] - quad[1]);
120 hodo[2] = 2 * (quad[4] - quad[2]);
121 hodo[3] = 2 * (quad[5] - quad[3]);
122 return hodo;
123 }
124
125 function quadraticRootsReal(A, B, C, s) {
126 if (A == 0) {
127 if (B == 0) {
128 s[0] = 0;
129 return C == 0;
130 }
131 s[0] = -C / B;
132 return 1;
133 }
134 /* normal form: x^2 + px + q = 0 */
135 var p = B / (2 * A);
136 var q = C / A;
137 var p2 = p * p;
138 if (p2 < q) {
139 return 0;
140 }
141 var sqrt_D = 0;
142 if (p2 > q) {
143 sqrt_D = sqrt(p2 - q);
144 }
145 s[0] = sqrt_D - p;
146 s[1] = -sqrt_D - p;
147 return 1 + s[0] != s[1];
148 }
149
150 function add_valid_ts(s, realRoots, t) {
151 var foundRoots = 0;
152 for (var index = 0; index < realRoots; ++index) {
153 var tValue = s[index];
154 if (tValue >= 0 && tValue <= 1) {
155 for (var idx2 = 0; idx2 < foundRoots; ++idx2) {
156 if (t[idx2] != tValue) {
157 t[foundRoots++] = tValue;
158 }
159 }
160 }
161 }
162 return foundRoots;
163 }
164
165 function quadraticRootsValidT(a, b, c, t) {
166 var s = [];
167 var realRoots = quadraticRootsReal(A, B, C, s);
168 var foundRoots = add_valid_ts(s, realRoots, t);
169 return foundRoots != 0;
170 }
171
172 function find_cubic_inflections(cubic, tValues)
173 {
174 var Ax = src[2] - src[0];
175 var Ay = src[3] - src[1];
176 var Bx = src[4] - 2 * src[2] + src[0];
177 var By = src[5] - 2 * src[3] + src[1];
178 var Cx = src[6] + 3 * (src[2] - src[4]) - src[0];
179 var Cy = src[7] + 3 * (src[3] - src[5]) - src[1];
180 return quadraticRootsValidT(Bx * Cy - By * Cx, (Ax * Cy - Ay * Cx),
181 Ax * By - Ay * Bx, tValues);
182 }
183
184 function dx_at_t(cubic, t) {
185 var one_t = 1 - t;
186 var a = cubic[0];
187 var b = cubic[2];
188 var c = cubic[4];
189 var d = cubic[6];
190 return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
191 }
192
193 function dy_at_t(cubic, t) {
194 var one_t = 1 - t;
195 var a = cubic[1];
196 var b = cubic[3];
197 var c = cubic[5];
198 var d = cubic[7];
199 return 3 * ((b - a) * one_t * one_t + 2 * (c - b) * t * one_t + (d - c) * t * t);
200 }
201
202 function x_at_t(cubic, t) {
203 var one_t = 1 - t;
204 var one_t2 = one_t * one_t;
205 var a = one_t2 * one_t;
206 var b = 3 * one_t2 * t;
207 var t2 = t * t;
208 var c = 3 * one_t * t2;
209 var d = t2 * t;
210 return a * cubic[0] + b * cubic[2] + c * cubic[4] + d * cubic[6];
211 }
212
213 function y_at_t(cubic, t) {
214 var one_t = 1 - t;
215 var one_t2 = one_t * one_t;
216 var a = one_t2 * one_t;
217 var b = 3 * one_t2 * t;
218 var t2 = t * t;
219 var c = 3 * one_t * t2;
220 var d = t2 * t;
221 return a * cubic[1] + b * cubic[3] + c * cubic[5] + d * cubic[7];
222 }
223
224 function calcFromScale() {
225 xStart = Math.floor(xmin * subscale) / subscale;
226 yStart = Math.floor(ymin * subscale) / subscale;
227 var xEnd = Math.ceil(xmin * subscale) / subscale;
228 var yEnd = Math.ceil(ymin * subscale) / subscale;
229 var cCelsW = Math.floor(ctx.canvas.width / 10);
230 var cCelsH = Math.floor(ctx.canvas.height / 10);
231 var testW = xEnd - xStart;
232 var testH = yEnd - yStart;
233 var scaleWH = 1;
234 while (cCelsW > testW * scaleWH * 10 && cCelsH > testH * scaleWH * 10) {
235 scaleWH *= 10;
236 }
237 while (cCelsW * 10 < testW * scaleWH && cCelsH * 10 < testH * scaleWH) {
238 scaleWH /= 10;
239 }
240
241 columns = Math.ceil(xmax * subscale) - Math.floor(xmin * subscale) + 1;
242 rows = Math.ceil(ymax * subscale) - Math.floor(ymin * subscale) + 1;
243
244 var hscale = ctx.canvas.width / columns / ticks;
245 var vscale = ctx.canvas.height / rows / ticks;
246 minScale = Math.floor(Math.min(hscale, vscale));
247 scale = minScale * subscale;
248 }
249
250 function drawLine(x1, y1, x2, y2) {
251 var unit = scale * ticks;
252 var xoffset = xStart * -unit + at_x;
253 var yoffset = yStart * -unit + at_y;
254 ctx.beginPath();
255 ctx.moveTo(xoffset + x1 * unit, yoffset + y1 * unit);
256 ctx.lineTo(xoffset + x2 * unit, yoffset + y2 * unit);
257 ctx.stroke();
258 }
259
260 function drawPoint(px, py) {
261 var unit = scale * ticks;
262 var xoffset = xStart * -unit + at_x;
263 var yoffset = yStart * -unit + at_y;
264 var _px = px * unit + xoffset;
265 var _py = py * unit + yoffset;
266 ctx.beginPath();
267 ctx.arc(_px, _py, 3, 0, Math.PI*2, true);
268 ctx.closePath();
269 ctx.stroke();
270 }
271
272 function drawPointSolid(px, py) {
273 drawPoint(px, py);
274 ctx.fillStyle = "rgba(0,0,0, 0.4)";
275 ctx.fill();
276 }
277
278 function drawLabel(num, px, py) {
279 ctx.beginPath();
280 ctx.arc(px, py, 8, 0, Math.PI*2, true);
281 ctx.closePath();
282 ctx.strokeStyle = "rgba(0,0,0, 0.4)";
283 ctx.lineWidth = num == 0 || num == 3 ? 2 : 1;
284 ctx.stroke();
285 ctx.fillStyle = "black";
286 ctx.font = "normal 10px Arial";
287 // ctx.rotate(0.001);
288 ctx.fillText(num, px - 2, py + 3);
289 // ctx.rotate(-0.001);
290 }
291
292 function drawLabelX(ymin, num, loc) {
293 var unit = scale * ticks;
294 var xoffset = xStart * -unit + at_x;
295 var yoffset = yStart * -unit + at_y;
296 var px = loc * unit + xoffset;
297 var py = ymin * unit + yoffset - 20;
298 drawLabel(num, px, py);
299 }
300
301 function drawLabelY(xmin, num, loc) {
302 var unit = scale * ticks;
303 var xoffset = xStart * -unit + at_x;
304 var yoffset = yStart * -unit + at_y;
305 var px = xmin * unit + xoffset - 20;
306 var py = loc * unit + yoffset;
307 drawLabel(num, px, py);
308 }
309
310 function drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY) {
311 ctx.beginPath();
312 ctx.moveTo(hx, hy - 100);
313 ctx.lineTo(hx, hy);
314 ctx.strokeStyle = hMinY < 0 ? "green" : "blue";
315 ctx.stroke();
316 ctx.beginPath();
317 ctx.moveTo(hx, hy);
318 ctx.lineTo(hx, hy + 100);
319 ctx.strokeStyle = hMaxY > 0 ? "green" : "blue";
320 ctx.stroke();
321 ctx.beginPath();
322 ctx.moveTo(hx - 100, hy);
323 ctx.lineTo(hx, hy);
324 ctx.strokeStyle = hMinX < 0 ? "green" : "blue";
325 ctx.stroke();
326 ctx.beginPath();
327 ctx.moveTo(hx, hy);
328 ctx.lineTo(hx + 100, hy);
329 ctx.strokeStyle = hMaxX > 0 ? "green" : "blue";
330 ctx.stroke();
331 }
332
333 function logCurves(test) {
334 for (curves in test) {
335 var curve = test[curves];
336 if (curve.length != 8) {
337 continue;
338 }
339 var str = "{{";
340 for (i = 0; i < 8; i += 2) {
341 str += curve[i].toFixed(2) + "," + curve[i + 1].toFixed(2);
342 if (i < 6) {
343 str += "}, {";
344 }
345 }
346 str += "}}";
347 console.log(str);
348 }
349 }
350
351 function scalexy(x, y, mag) {
352 var length = Math.sqrt(x * x + y * y);
353 return mag / length;
354 }
355
356 function drawArrow(x, y, dx, dy) {
357 var unit = scale * ticks;
358 var xoffset = xStart * -unit + at_x;
359 var yoffset = yStart * -unit + at_y;
360 var dscale = scalexy(dx, dy, 1);
361 dx *= dscale;
362 dy *= dscale;
363 ctx.beginPath();
364 ctx.moveTo(xoffset + x * unit, yoffset + y * unit);
365 x += dx;
366 y += dy;
367 ctx.lineTo(xoffset + x * unit, yoffset + y * unit);
368 dx /= 10;
369 dy /= 10;
370 ctx.lineTo(xoffset + (x - dy) * unit, yoffset + (y + dx) * unit);
371 ctx.lineTo(xoffset + (x + dx * 2) * unit, yoffset + (y + dy * 2) * unit);
372 ctx.lineTo(xoffset + (x + dy) * unit, yoffset + (y - dx) * unit);
373 ctx.lineTo(xoffset + x * unit, yoffset + y * unit);
374 ctx.strokeStyle = "rgba(0,75,0, 0.4)";
375 ctx.stroke();
376 }
377
378 function draw(test, title) {
379 ctx.fillStyle = "rgba(0,0,0, 0.1)";
380 ctx.font = "normal 50px Arial";
381 ctx.fillText(title, 50, 50);
382 ctx.font = "normal 10px Arial";
383 var unit = scale * ticks;
384 // ctx.lineWidth = "1.001"; "0.999";
385 var xoffset = xStart * -unit + at_x;
386 var yoffset = yStart * -unit + at_y;
387
388 for (curves in test) {
389 var curve = test[curves];
390 if (curve.length != 8) {
391 continue;
392 }
393 ctx.lineWidth = 1;
394 if (draw_tangents) {
395 ctx.strokeStyle = "rgba(0,0,255, 0.3)";
396 drawLine(curve[0], curve[1], curve[2], curve[3]);
397 drawLine(curve[2], curve[3], curve[4], curve[5]);
398 drawLine(curve[4], curve[5], curve[6], curve[7]);
399 }
400 if (draw_deriviatives) {
401 var dx = dx_at_t(curve, 0);
402 var dy = dy_at_t(curve, 0);
403 drawArrow(curve[0], curve[1], dx, dy);
404 dx = dx_at_t(curve, 1);
405 dy = dy_at_t(curve, 1);
406 drawArrow(curve[6], curve[7], dx, dy);
407 if (draw_midpoint) {
408 var midX = x_at_t(curve, 0.5);
409 var midY = y_at_t(curve, 0.5);
410 dx = dx_at_t(curve, 0.5);
411 dy = dy_at_t(curve, 0.5);
412 drawArrow(midX, midY, dx, dy);
413 }
414 }
415 ctx.beginPath();
416 ctx.moveTo(xoffset + curve[0] * unit, yoffset + curve[1] * unit);
417 ctx.bezierCurveTo(
418 xoffset + curve[2] * unit, yoffset + curve[3] * unit,
419 xoffset + curve[4] * unit, yoffset + curve[5] * unit,
420 xoffset + curve[6] * unit, yoffset + curve[7] * unit);
421 ctx.strokeStyle = "black";
422 ctx.stroke();
423 if (draw_endpoints) {
424 drawPoint(curve[0], curve[1]);
425 drawPoint(curve[2], curve[3]);
426 drawPoint(curve[4], curve[5]);
427 drawPoint(curve[6], curve[7]);
428 }
429 if (draw_midpoint) {
430 var midX = x_at_t(curve, 0.5);
431 var midY = y_at_t(curve, 0.5);
432 drawPointSolid(midX, midY);
433 }
434 if (draw_hodo) {
435 var hodo = hodograph(curve);
436 var hMinX = Math.min(0, hodo[0], hodo[2], hodo[4]);
437 var hMinY = Math.min(0, hodo[1], hodo[3], hodo[5]);
438 var hMaxX = Math.max(0, hodo[0], hodo[2], hodo[4]);
439 var hMaxY = Math.max(0, hodo[1], hodo[3], hodo[5]);
440 var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1;
441 var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY ) : 1;
442 var hUnit = Math.min(hScaleX, hScaleY);
443 hUnit /= 2;
444 var hx = xoffset - hMinX * hUnit;
445 var hy = yoffset - hMinY * hUnit;
446 ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit);
447 ctx.quadraticCurveTo(
448 hx + hodo[2] * hUnit, hy + hodo[3] * hUnit,
449 hx + hodo[4] * hUnit, hy + hodo[5] * hUnit);
450 ctx.strokeStyle = "red";
451 ctx.stroke();
452 if (draw_hodo_origin) {
453 drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY);
454 }
455 }
456 if (draw_hodo2) {
457 var hodo = hodograph2(curve);
458 var hMinX = Math.min(0, hodo[0], hodo[2]);
459 var hMinY = Math.min(0, hodo[1], hodo[3]);
460 var hMaxX = Math.max(0, hodo[0], hodo[2]);
461 var hMaxY = Math.max(0, hodo[1], hodo[3]);
462 var hScaleX = hMaxX - hMinX > 0 ? ctx.canvas.width / (hMaxX - hMinX) : 1;
463 var hScaleY = hMaxY - hMinY > 0 ? ctx.canvas.height / (hMaxY - hMinY ) : 1;
464 var hUnit = Math.min(hScaleX, hScaleY);
465 hUnit /= 2;
466 var hx = xoffset - hMinX * hUnit;
467 var hy = yoffset - hMinY * hUnit;
468 ctx.moveTo(hx + hodo[0] * hUnit, hy + hodo[1] * hUnit);
469 ctx.lineTo(hx + hodo[2] * hUnit, hy + hodo[3] * hUnit);
470 ctx.strokeStyle = "red";
471 ctx.stroke();
472 drawHodoOrigin(hx, hy, hMinX, hMinY, hMaxX, hMaxY);
473 }
474 if (draw_sequence) {
475 var ymin = Math.min(curve[1], curve[3], curve[5], curve[7]);
476 for (var i = 0; i < 8; i+= 2) {
477 drawLabelX(ymin, i >> 1, curve[i]);
478 }
479 var xmin = Math.min(curve[0], curve[2], curve[4], curve[6]);
480 for (var i = 1; i < 8; i+= 2) {
481 drawLabelY(xmin, i >> 1, curve[i]);
482 }
483 }
484 }
485 }
486
487 function drawTop() {
488 init(tests[testIndex]);
489 redraw();
490 }
491
492 function redraw() {
493 ctx.beginPath();
494 ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
495 ctx.fillStyle="white";
496 ctx.fill();
497 draw(tests[testIndex], testTitles[testIndex]);
498 }
499
500 function doKeyPress(evt) {
501 var char = String.fromCharCode(evt.charCode);
502 switch (char) {
503 case '2':
504 draw_hodo2 ^= true;
505 redraw();
506 break;
507 case 'd':
508 draw_deriviatives ^= true;
509 redraw();
510 break;
511 case 'e':
512 draw_endpoints ^= true;
513 redraw();
514 break;
515 case 'h':
516 draw_hodo ^= true;
517 redraw();
518 break;
519 case 'N':
520 testIndex += 9;
521 case 'n':
522 if (++testIndex >= tests.length)
523 testIndex = 0;
524 drawTop();
525 break;
526 case 'l':
527 logCurves(tests[testIndex]);
528 break;
529 case 'm':
530 draw_midpoint ^= true;
531 redraw();
532 break;
533 case 'o':
534 draw_hodo_origin ^= true;
535 redraw();
536 break;
537 case 'P':
538 testIndex -= 9;
539 case 'p':
540 if (--testIndex < 0)
541 testIndex = tests.length - 1;
542 drawTop();
543 break;
544 case 's':
545 draw_sequence ^= true;
546 redraw();
547 break;
548 case 't':
549 draw_tangents ^= true;
550 redraw();
551 break;
552 }
553 }
554
555 function calcXY() {
556 var e = window.event;
557 var tgt = e.target || e.srcElement;
558 var left = tgt.offsetLeft;
559 var top = tgt.offsetTop;
560 var unit = scale * ticks;
561 mouseX = (e.clientX - left - Math.ceil(at_x) + 1) / unit + xStart;
562 mouseY = (e.clientY - top - Math.ceil(at_y)) / unit + yStart;
563 }
564
565 var lastX, lastY;
566 var activeCurve = [];
567 var activePt;
568
569 function handleMouseClick() {
570 calcXY();
571 }
572
573 function initDown() {
574 var unit = scale * ticks;
575 var xoffset = xStart * -unit + at_x;
576 var yoffset = yStart * -unit + at_y;
577 var test = tests[testIndex];
578 var bestDistance = 1000000;
579 activePt = -1;
580 for (curves in test) {
581 var testCurve = test[curves];
582 if (testCurve.length != 8) {
583 continue;
584 }
585 for (var i = 0; i < 8; i += 2) {
586 var testX = testCurve[i];
587 var testY = testCurve[i + 1];
588 var dx = testX - mouseX;
589 var dy = testY - mouseY;
590 var dist = dx * dx + dy * dy;
591 if (dist > bestDistance) {
592 continue;
593 }
594 activeCurve = testCurve;
595 activePt = i;
596 bestDistance = dist;
597 }
598 }
599 if (activePt >= 0) {
600 lastX = mouseX;
601 lastY = mouseY;
602 }
603 }
604
605 function handleMouseOver() {
606 if (!mouseDown) {
607 activePt = -1;
608 return;
609 }
610 calcXY();
611 if (activePt < 0) {
612 initDown();
613 return;
614 }
615 var unit = scale * ticks;
616 var deltaX = (mouseX - lastX) /* / unit */;
617 var deltaY = (mouseY - lastY) /*/ unit */;
618 lastX = mouseX;
619 lastY = mouseY;
620 activeCurve[activePt] += deltaX;
621 activeCurve[activePt + 1] += deltaY;
622 redraw();
623 }
624
625 function start() {
626 for (i = 0; i < testDivs.length; ++i) {
627 var title = testDivs[i].id.toString();
628 var str = testDivs[i].firstChild.data;
629 parse(str, title);
630 }
631 drawTop();
632 window.addEventListener('keypress', doKeyPress, true);
633 window.onresize = function() {
634 drawTop();
635 }
636 }
637
638 </script>
639 </head>
640
641 <body onLoad="start();">
642 <canvas id="canvas" width="750" height="500"
643 onmousedown="mouseDown = true"
644 onmouseup="mouseDown = false"
645 onmousemove="handleMouseOver()"
646 onclick="handleMouseClick()"
647 ></canvas >
648 </body>
649 </html>
OLDNEW
« no previous file with comments | « experimental/Intersection/edge_Prefix.pch ('k') | experimental/Intersection/op.htm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698