Title: [242809] trunk
Revision
242809
Author
[email protected]
Date
2019-03-12 11:59:29 -0700 (Tue, 12 Mar 2019)

Log Message

Web Inspector: Canvas: export recording as HTML
https://bugs.webkit.org/show_bug.cgi?id=195311
<rdar://problem/48588673>

Reviewed by Joseph Pecoraro.

Source/WebInspectorUI:

* UserInterface/Models/Recording.js:
(WI.Recording.prototype.async swizzle):
(WI.Recording.prototype.toHTML): Added.
(WI.Recording.prototype.toHTML.escapeHTML): Added.
(WI.Recording.prototype.toHTML.processObject): Added.
(WI.Recording.prototype.toHTML.processValue): Added.

* UserInterface/Views/RecordingContentView.js:
(WI.RecordingContentView):
(WI.RecordingContentView.prototype._exportRecording):
(WI.RecordingContentView.prototype._exportReduction): Added.
(WI.RecordingContentView.prototype._updateExportButton): Added.
(WI.RecordingContentView.prototype._handleExportNavigationItemClicked): Added.
(WI.RecordingContentView.prototype._handleRecordingProcessedAction):

* UserInterface/Views/CanvasOverviewContentView.js:
(WI.CanvasOverviewContentView):
* UserInterface/Views/HeapAllocationsTimelineView.js:
(WI.HeapAllocationsTimelineView):
* UserInterface/Views/HeapSnapshotContentView.js:
(WI.HeapSnapshotContentView):
Drive-by: s/`toolTip`/`tooltip`.

* Localizations/en.lproj/localizedStrings.js:

LayoutTests:

* inspector/canvas/recording-html-2d.html: Added.
* inspector/canvas/recording-html-2d-expected.txt: Added.
* inspector/canvas/recording-2d-expected.txt: Added.
* inspector/canvas/resources/recording-utilities.js:
(TestPage.registerInitializer.log):
(TestPage.registerInitializer.window.startRecording):

* platform/ios-wk1/TestExpectations:
* platform/mac-wk1/TestExpectations:

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (242808 => 242809)


--- trunk/LayoutTests/ChangeLog	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/LayoutTests/ChangeLog	2019-03-12 18:59:29 UTC (rev 242809)
@@ -1,5 +1,23 @@
 2019-03-12  Devin Rousso  <[email protected]>
 
+        Web Inspector: Canvas: export recording as HTML
+        https://bugs.webkit.org/show_bug.cgi?id=195311
+        <rdar://problem/48588673>
+
+        Reviewed by Joseph Pecoraro.
+
+        * inspector/canvas/recording-html-2d.html: Added.
+        * inspector/canvas/recording-html-2d-expected.txt: Added.
+        * inspector/canvas/recording-2d-expected.txt: Added.
+        * inspector/canvas/resources/recording-utilities.js:
+        (TestPage.registerInitializer.log):
+        (TestPage.registerInitializer.window.startRecording):
+
+        * platform/ios-wk1/TestExpectations:
+        * platform/mac-wk1/TestExpectations:
+
+2019-03-12  Devin Rousso  <[email protected]>
+
         Web Inspector: Audit: there should be a centralized place for reusable code
         https://bugs.webkit.org/show_bug.cgi?id=195265
         <rdar://problem/47040673>

Modified: trunk/LayoutTests/inspector/canvas/recording-2d-expected.txt (242808 => 242809)


--- trunk/LayoutTests/inspector/canvas/recording-2d-expected.txt	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/LayoutTests/inspector/canvas/recording-2d-expected.txt	2019-03-12 18:59:29 UTC (rev 242809)
@@ -29,7 +29,7 @@
     fillStyle: "#000000"
     imageSmoothingEnabled: true
     imageSmoothingQuality: "low"
-    setPath: [{}]
+    setPath: [""]
   parameters:
   content: <filtered>
 frames:
@@ -88,7 +88,7 @@
     fillStyle: "#000000"
     imageSmoothingEnabled: true
     imageSmoothingQuality: "low"
-    setPath: [{}]
+    setPath: [""]
   parameters:
   content: <filtered>
 frames:
@@ -1115,7 +1115,7 @@
     fillStyle: "#000000"
     imageSmoothingEnabled: true
     imageSmoothingQuality: "low"
-    setPath: [{}]
+    setPath: [""]
   parameters:
   content: <filtered>
 frames:

Added: trunk/LayoutTests/inspector/canvas/recording-html-2d-expected.txt (0 => 242809)


--- trunk/LayoutTests/inspector/canvas/recording-html-2d-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/inspector/canvas/recording-html-2d-expected.txt	2019-03-12 18:59:29 UTC (rev 242809)
@@ -0,0 +1,240 @@
+Tests for Recording.prototype.toHTML.
+
+
+== Running test suite: Recording.prototype.toHTML
+-- Running test case: Recording.prototype.toHTML.2D
+++++++++++ HTML ++++++++++
+<!DOCTYPE html>
+<head>
+<title>Recording 1</title>
+<style>
+    body {
+        margin: 0;
+    }
+    canvas {
+        max-width: calc(100% - 40px);
+        max-height: calc(100% - 40px);
+        padding: 20px;
+    }
+</style>
+</head>
+<body>
+<script>
+"use strict";
+
+let promises = [];
+let objects = {};
+
+let canvas = document.body.appendChild(document.createElement("canvas"));
+canvas.width = 2;
+canvas.height = 2;
+
+let context = canvas.getContext("2d");
+
+let frames = [
+    function initialState() {
+        context.drawImage(objects[0], 0, 0);
+
+        context.direction = "ltr";
+        context.fillStyle = "#000000";
+        context.font = "10px sans-serif";
+        context.globalAlpha = 1;
+        context.globalCompositeOperation = "source-over";
+        context.imageSmoothingEnabled = true;
+        context.imageSmoothingQuality = "low";
+        context.lineCap = "butt";
+        context.lineDash = [];
+        context.lineDashOffset = 0;
+        context.lineJoin = "miter";
+        context.lineWidth = 1;
+        context.miterLimit = 10;
+        context.shadowBlur = 0;
+        context.shadowColor = "rgba(0, 0, 0, 0)";
+        context.shadowOffsetX = 0;
+        context.shadowOffsetY = 0;
+        context.strokeStyle = "#ff0000";
+        context.textAlign = "start";
+        context.textBaseline = "alphabetic";
+        context.transform(1, 0, 0, 1, 0, 0);
+        context.webkitImageSmoothingEnabled = true;
+        context.webkitLineDash = [];
+        context.webkitLineDashOffset = 0;
+        if ("setPath" in context)
+            context.setPath(objects[1]);
+        context.save();
+
+        context.direction = "ltr";
+        context.fillStyle = "#000000";
+        context.font = "10px sans-serif";
+        context.globalAlpha = 1;
+        context.globalCompositeOperation = "source-over";
+        context.imageSmoothingEnabled = true;
+        context.imageSmoothingQuality = "low";
+        context.lineCap = "butt";
+        context.lineDash = [];
+        context.lineDashOffset = 0;
+        context.lineJoin = "miter";
+        context.lineWidth = 1;
+        context.miterLimit = 10;
+        context.shadowBlur = 0;
+        context.shadowColor = "rgba(0, 0, 0, 0)";
+        context.shadowOffsetX = 0;
+        context.shadowOffsetY = 0;
+        context.strokeStyle = "#0000ff";
+        context.textAlign = "start";
+        context.textBaseline = "alphabetic";
+        context.transform(1, 0, 0, 1, 0, 0);
+        context.webkitImageSmoothingEnabled = true;
+        context.webkitLineDash = [];
+        context.webkitLineDashOffset = 0;
+        if ("setPath" in context)
+            context.setPath(objects[2]);
+        context.save();
+
+        context.direction = "ltr";
+        context.fillStyle = "#000000";
+        context.font = "10px sans-serif";
+        context.globalAlpha = 1;
+        context.globalCompositeOperation = "source-over";
+        context.imageSmoothingEnabled = true;
+        context.imageSmoothingQuality = "low";
+        context.lineCap = "butt";
+        context.lineDash = [];
+        context.lineDashOffset = 0;
+        context.lineJoin = "miter";
+        context.lineWidth = 1;
+        context.miterLimit = 10;
+        context.shadowBlur = 0;
+        context.shadowColor = "rgba(0, 0, 0, 0)";
+        context.shadowOffsetX = 0;
+        context.shadowOffsetY = 0;
+        context.strokeStyle = "#008000";
+        context.textAlign = "start";
+        context.textBaseline = "alphabetic";
+        context.transform(1, 0, 0, 1, 0, 0);
+        context.webkitImageSmoothingEnabled = true;
+        context.webkitLineDash = [];
+        context.webkitLineDashOffset = 0;
+        if ("setPath" in context)
+            context.setPath(objects[3]);
+    },
+    function startRecording() {
+        if (typeof console.record === "function")
+            console.record(context, {name: "Recording 1"});
+    },
+    function frame1() {
+        context.fillStyle;
+        context.fillStyle = "test";
+        context.fillStyle = objects[4];
+        context.fillStyle = objects[5];
+        context.fillStyle = objects[6];
+    },
+    function frame2() {
+        context.beginPath();
+    },
+    function frame3() {
+        context.moveTo(1, 2);
+    },
+    function frame4() {
+        context.drawImage(objects[7], 11, 12);
+    },
+    function frame5() {
+        context.createImageData(objects[8]);
+    },
+    function frame6() {
+        context.drawImage(objects[9], 11, 12);
+    },
+    function frame7() {
+        context.setTransform(objects[10]);
+    },
+    function frame8() {
+        // context.drawFocusIfNeeded(objects[11], "Element");
+    },
+    function stopRecording() {
+        if (typeof console.recordEnd === "function")
+            console.recordEnd(context);
+    },
+];
+
+function rebuildCanvasGradient(key, data) {
+    let gradient = null;
+    if (data.type === "radial-gradient")
+        gradient = context.createRadialGradient(data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]);
+    else
+        gradient = context.createLinearGradient(data.points[0], data.points[1], data.points[2], data.points[3]);
+    for (let stop of data.stops)
+        gradient.addColorStop(stop.offset, stop.color);
+    objects[key] = gradient;
+}
+function rebuildCanvasPattern(key, data) {
+    promises.push(new Promise(function(resolve, reject) {
+        let image = new Image;
+        function resolveWithImage(event) {
+            objects[key] = context.createPattern(image, data.repeat);
+            resolve();
+        }
+        image.addEventListener("load", resolveWithImage);
+        image.addEventListener("error", resolveWithImage);
+        image.src = ""
+    }));
+}
+function rebuildDOMMatrix(key, data) {
+    objects[key] = new DOMMatrix(data);
+}
+function rebuildImage(key, data) {
+    promises.push(new Promise(function(resolve, reject) {
+        let image = new Image;
+        function resolveWithImage(event) {
+            objects[key] = image;
+            resolve();
+        }
+        image.addEventListener("load", resolveWithImage);
+        image.addEventListener("error", resolveWithImage);
+        image.src = ""
+    }));
+}
+function rebuildImageBitmap(key, data) {
+    promises.push(new Promise(function(resolve, reject) {
+        let image = new Image;
+        function resolveWithImage(event) {
+            createImageBitmap(image).then(function(imageBitmap) {
+                objects[key] = imageBitmap;
+                resolve();
+            });
+        }
+        image.addEventListener("load", resolveWithImage);
+        image.addEventListener("error", resolveWithImage);
+        image.src = ""
+    }));
+}
+function rebuildImageData(key, data) {
+    objects[key] = new ImageData(new Uint8ClampedArray(data.data), parseInt(data.width), parseInt(data.height));
+}
+function rebuildPath2D(key, data) {
+    objects[key] = new Path2D(data);
+}
+
+rebuildImage(0, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC");
+rebuildPath2D(1, "");
+rebuildPath2D(2, "");
+rebuildPath2D(3, "");
+rebuildCanvasGradient(4, {"type":"linear-gradient","points":[1,2,3,4],"stops":[{"offset":1,"color":"rgb(255, 0, 0)"},{"offset":1,"color":"rgb(0, 0, 255)"}]});
+rebuildCanvasGradient(5, {"type":"radial-gradient","points":[1,2,3,4,5,6],"stops":[]});
+rebuildCanvasPattern(6, {"image":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII=","repeat":"no-repeat"});
+rebuildImage(7, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII=");
+rebuildImageData(8, {"data":[0,0,0,0,0,0,0,0],"width":1,"height":2});
+rebuildImageBitmap(9, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII=");
+rebuildDOMMatrix(10, "matrix(1, 2, 3, 4, 5, 6)");
+rebuildPath2D(11, "M1 2");
+
+Promise.all(promises).then(function() {
+    window.requestAnimationFrame(function executeFrame() {
+        frames.shift()();
+        if (frames.length)
+            window.requestAnimationFrame(executeFrame);
+    });
+});
+</script>
+</body>
+++++++++++ HTML ++++++++++
+

Added: trunk/LayoutTests/inspector/canvas/recording-html-2d.html (0 => 242809)


--- trunk/LayoutTests/inspector/canvas/recording-html-2d.html	                        (rev 0)
+++ trunk/LayoutTests/inspector/canvas/recording-html-2d.html	2019-03-12 18:59:29 UTC (rev 242809)
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script src=""
+<script src=""
+<script>
+let context = null;
+
+let canvas = document.createElement("canvas");
+canvas.width = 2;
+canvas.height = 2;
+
+// 2x2 red square
+let image = document.createElement("img");
+image.src = ""
+
+let linearGradient = null;
+
+let radialGradient = null;
+
+let pattern = null;
+
+let path2D = new Path2D("M 1 2");
+
+let imageData = new ImageData(1, 2);
+
+let imageBitmap = null;
+
+async function load() {
+    context = canvas.getContext("2d");
+
+    linearGradient = context.createLinearGradient(1, 2, 3, 4);
+    linearGradient.addColorStop(1, "red");
+
+    radialGradient = context.createRadialGradient(1, 2, 3, 4, 5, 6);
+    linearGradient.addColorStop(1, "blue");
+
+    pattern = context.createPattern(image, "no-repeat");
+
+    imageBitmap = await createImageBitmap(image);
+
+    document.body.appendChild(canvas);
+
+    context.strokeStyle = "red";
+    context.save();
+    context.strokeStyle = "blue";
+    context.save();
+    context.strokeStyle = "green";
+
+    runTest();
+}
+
+function ignoreException(func){
+    try {
+        func();
+    } catch (e) { }
+}
+
+function cancelActions() {
+}
+
+function performActions() {
+    let frames = [
+        () => {
+            context.fillStyle;
+            context.fillStyle = "test";
+            context.fillStyle = linearGradient;
+            context.fillStyle = radialGradient;
+            context.fillStyle = pattern;
+        },
+        () => {
+            context.beginPath();
+        },
+        () => {
+            ignoreException(() => context.moveTo(1, 2));
+        },
+        () => {
+            ignoreException(() => context.drawImage(image, 11, 12));
+        },
+        () => {
+            ignoreException(() => context.createImageData(imageData));
+        },
+        () => {
+            ignoreException(() => context.drawImage(imageBitmap, 11, 12));
+        },
+        () => {
+            ignoreException(() => context.setTransform(new DOMMatrix([1, 2, 3, 4, 5, 6])));
+        },
+        () => {
+            context.drawFocusIfNeeded(path2D, document.body);
+        },
+        () => {
+            TestPage.dispatchEventToFrontend("LastFrame");
+        },
+    ];
+    let index = 0;
+    function executeFrameFunction() {
+        frames[index++]();
+        if (index < frames.length)
+            timeoutID = setTimeout(executeFrameFunction, 0);
+    };
+    executeFrameFunction();
+}
+
+function test() {
+    let suite = InspectorTest.createAsyncSuite("Recording.prototype.toHTML");
+
+    suite.addTestCase({
+        name: "Recording.prototype.toHTML.2D",
+        test(resolve, reject) {
+            startRecording(WI.Canvas.ContextType.Canvas2D, resolve, reject, {
+                callback(recording) {
+                    let promise = new WI.WrappedPromise;
+
+                    let listener = recording.addEventListener(WI.Recording.Event.ProcessedAction, (event) => {
+                        if (!recording.ready)
+                            return;
+
+                        InspectorTest.log("++++++++++ HTML ++++++++++");
+                        InspectorTest.log(recording.toHTML());
+                        InspectorTest.log("++++++++++ HTML ++++++++++");
+
+                        recording.removeEventListener(WI.Recording.Event.ProcessedAction, listener);
+                        promise.resolve();
+                    });
+
+                    recording.startProcessing();
+
+                    return promise.promise;
+                },
+            });
+        },
+    });
+
+    suite.runTestCasesAndFinish();
+}
+</script>
+</head>
+<body _onload_="load()">
+    <p>Tests for Recording.prototype.toHTML.</p>
+</body>
+</html>

Modified: trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js (242808 => 242809)


--- trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -3,8 +3,12 @@
         for (let [name, value] of object) {
             if (typeof value === "string")
                 value = sanitizeURL(value);
-            else if (Array.isArray(value) && value[0] instanceof DOMMatrix)
-                value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
+            else if (Array.isArray(value)) {
+                if (value[0] instanceof DOMMatrix)
+                    value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
+                else if (value[0] instanceof Path2D)
+                    value[0] = value[0].__data;
+            }
             InspectorTest.log(indent + name + ": " + JSON.stringify(value));
         }
     }
@@ -89,7 +93,7 @@
         return canvases[0];
     };
 
-    window.startRecording = function(type, resolve, reject, {frameCount, memoryLimit, checkForContentChange} = {}) {
+    window.startRecording = function(type, resolve, reject, {frameCount, memoryLimit, checkForContentChange, callback} = {}) {
         let canvas = getCanvas(type);
         if (!canvas) {
             reject(`Missing canvas with type "${type}".`);
@@ -143,7 +147,7 @@
             Promise.all(recording.actions.map((action) => action.swizzle(recording))).then(() => {
                 swizzled = true;
 
-                logRecording(recording, {checkForContentChange})
+                (callback ? callback(recording) : logRecording(recording, {checkForContentChange}))
                 .then(() => {
                     if (lastFrame) {
                         InspectorTest.evaluateInPage(`cancelActions()`)

Modified: trunk/LayoutTests/platform/ios-wk1/TestExpectations (242808 => 242809)


--- trunk/LayoutTests/platform/ios-wk1/TestExpectations	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/LayoutTests/platform/ios-wk1/TestExpectations	2019-03-12 18:59:29 UTC (rev 242809)
@@ -1998,3 +1998,6 @@
 css-typedom [ Skip ]
 
 webkit.org/b/188762 fast/writing-mode/english-rl-text-with-spelling-marker.html [ ImageOnlyFailure ]
+
+# WK1 Inspector cannot leverage InspectorAdditions runtime enabled features
+inspector/canvas/recording-html-2d.html

Modified: trunk/LayoutTests/platform/mac-wk1/TestExpectations (242808 => 242809)


--- trunk/LayoutTests/platform/mac-wk1/TestExpectations	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/LayoutTests/platform/mac-wk1/TestExpectations	2019-03-12 18:59:29 UTC (rev 242809)
@@ -291,6 +291,9 @@
 inspector/unit-tests/heap-snapshot.html [ Skip ]
 inspector/unit-tests/heap-snapshot-collection-event.html [ Skip ]
 
+# WK1 Inspector cannot leverage InspectorAdditions runtime enabled features
+inspector/canvas/recording-html-2d.html
+
 webkit.org/b/188708 inspector/dom-debugger/event-breakpoint-with-navigation.html [ Pass Timeout ]
 
 # These tests check ScrollAnimator events for main frame scrollbars that use native widgets in WK1.

Modified: trunk/Source/WebInspectorUI/ChangeLog (242808 => 242809)


--- trunk/Source/WebInspectorUI/ChangeLog	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/ChangeLog	2019-03-12 18:59:29 UTC (rev 242809)
@@ -1,5 +1,38 @@
 2019-03-12  Devin Rousso  <[email protected]>
 
+        Web Inspector: Canvas: export recording as HTML
+        https://bugs.webkit.org/show_bug.cgi?id=195311
+        <rdar://problem/48588673>
+
+        Reviewed by Joseph Pecoraro.
+
+        * UserInterface/Models/Recording.js:
+        (WI.Recording.prototype.async swizzle):
+        (WI.Recording.prototype.toHTML): Added.
+        (WI.Recording.prototype.toHTML.escapeHTML): Added.
+        (WI.Recording.prototype.toHTML.processObject): Added.
+        (WI.Recording.prototype.toHTML.processValue): Added.
+
+        * UserInterface/Views/RecordingContentView.js:
+        (WI.RecordingContentView):
+        (WI.RecordingContentView.prototype._exportRecording):
+        (WI.RecordingContentView.prototype._exportReduction): Added.
+        (WI.RecordingContentView.prototype._updateExportButton): Added.
+        (WI.RecordingContentView.prototype._handleExportNavigationItemClicked): Added.
+        (WI.RecordingContentView.prototype._handleRecordingProcessedAction):
+
+        * UserInterface/Views/CanvasOverviewContentView.js:
+        (WI.CanvasOverviewContentView):
+        * UserInterface/Views/HeapAllocationsTimelineView.js:
+        (WI.HeapAllocationsTimelineView):
+        * UserInterface/Views/HeapSnapshotContentView.js:
+        (WI.HeapSnapshotContentView):
+        Drive-by: s/`toolTip`/`tooltip`.
+
+        * Localizations/en.lproj/localizedStrings.js:
+
+2019-03-12  Devin Rousso  <[email protected]>
+
         Web Inspector: Audit: there should be a centralized place for reusable code
         https://bugs.webkit.org/show_bug.cgi?id=195265
         <rdar://problem/47040673>

Modified: trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -448,6 +448,7 @@
 localizedStrings["Export Result"] = "Export Result";
 localizedStrings["Export Test"] = "Export Test";
 localizedStrings["Export recording (%s)"] = "Export recording (%s)";
+localizedStrings["Export recording (%s)\nShift-click to export a HTML reduction"] = "Export recording (%s)\nShift-click to export a HTML reduction";
 localizedStrings["Export result (%s)"] = "Export result (%s)";
 localizedStrings["_expression_"] = "_expression_";
 localizedStrings["Extension Scripts"] = "Extension Scripts";

Modified: trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -339,31 +339,57 @@
 
                 case WI.Recording.Swizzle.Image:
                     this._swizzle[index][type] = await WI.ImageUtilities.promisifyLoad(data);
+                    this._swizzle[index][type].__data = data;
                     break;
 
-                case WI.Recording.Swizzle.ImageData:
-                    this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(data[0]), parseInt(data[1]), parseInt(data[2]));
+                case WI.Recording.Swizzle.ImageData: {
+                    let [object, width, height] = await Promise.all([
+                        this.swizzle(data[0], WI.Recording.Swizzle.Array),
+                        this.swizzle(data[1], WI.Recording.Swizzle.Number),
+                        this.swizzle(data[2], WI.Recording.Swizzle.Number),
+                    ]);
+
+                    object = await Promise.all(object.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number)));
+
+                    this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(object), width, height);
+                    this._swizzle[index][type].__data = {data: object, width, height};
                     break;
+                }
 
                 case WI.Recording.Swizzle.Path2D:
                     this._swizzle[index][type] = new Path2D(data);
+                    this._swizzle[index][type].__data = data;
                     break;
 
-                case WI.Recording.Swizzle.CanvasGradient:
-                    var gradientType = await this.swizzle(data[0], WI.Recording.Swizzle.String);
+                case WI.Recording.Swizzle.CanvasGradient: {
+                    let [gradientType, points] = await Promise.all([
+                        this.swizzle(data[0], WI.Recording.Swizzle.String),
+                        this.swizzle(data[1], WI.Recording.Swizzle.Array),
+                    ]);
 
+                    points = await Promise.all(points.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number)));
+
                     WI.ImageUtilities.scratchCanvasContext2D((context) => {
-                        this._swizzle[index][type] = gradientType === "radial-gradient" ? context.createRadialGradient(...data[1]) : context.createLinearGradient(...data[1]);
+                        this._swizzle[index][type] = gradientType === "radial-gradient" ? context.createRadialGradient(...points) : context.createLinearGradient(...points);
                     });
 
+                    let stops = [];
                     for (let stop of data[2]) {
-                        let color = await this.swizzle(stop[1], WI.Recording.Swizzle.String);
-                        this._swizzle[index][type].addColorStop(stop[0], color);
+                        let [offset, color] = await Promise.all([
+                            this.swizzle(stop[0], WI.Recording.Swizzle.Number),
+                            this.swizzle(stop[1], WI.Recording.Swizzle.String),
+                        ]);
+                        this._swizzle[index][type].addColorStop(offset, color);
+
+                        stops.push({offset, color});
                     }
+
+                    this._swizzle[index][type].__data = {type: gradientType, points, stops};
                     break;
+                }
 
-                case WI.Recording.Swizzle.CanvasPattern:
-                    var [image, repeat] = await Promise.all([
+                case WI.Recording.Swizzle.CanvasPattern: {
+                    let [image, repeat] = await Promise.all([
                         this.swizzle(data[0], WI.Recording.Swizzle.Image),
                         this.swizzle(data[1], WI.Recording.Swizzle.String),
                     ]);
@@ -372,12 +398,17 @@
                         this._swizzle[index][type] = context.createPattern(image, repeat);
                         this._swizzle[index][type].__image = image;
                     });
+
+                    this._swizzle[index][type].__data = {image: image.__data, repeat};
                     break;
+                }
 
-                case WI.Recording.Swizzle.ImageBitmap:
-                    var image = await this.swizzle(index, WI.Recording.Swizzle.Image);
+                case WI.Recording.Swizzle.ImageBitmap: {
+                    let image = await this.swizzle(index, WI.Recording.Swizzle.Image);
                     this._swizzle[index][type] = await createImageBitmap(image);
+                    this._swizzle[index][type].__data = data;
                     break;
+                }
 
                 case WI.Recording.Swizzle.CallStack: {
                     let array = await this.swizzle(data, WI.Recording.Swizzle.Array);
@@ -451,6 +482,274 @@
         };
     }
 
+    toHTML()
+    {
+        console.assert(this._type === WI.Recording.Type.Canvas2D);
+        console.assert(this.ready);
+
+        let lines = [];
+        let objects = [];
+
+        function processObject(object) {
+            objects.push({object, index: objects.length});
+            return `objects[${objects.length - 1}]`;
+        }
+
+        function processValue(value) {
+            if (typeof value === "object" && !Array.isArray(value))
+                return processObject(value);
+            return JSON.stringify(value);
+        }
+
+        function escapeHTML(s) {
+            return s.replace(/[^0-9A-Za-z ]/g, (c) => {
+                return `&#${c.charCodeAt(0)};`;
+            });
+        }
+
+        lines.push(`<!DOCTYPE html>`);
+        lines.push(`<head>`);
+        lines.push(`<title>${escapeHTML(this._displayName)}</title>`);
+        lines.push(`<style>`);
+        lines.push(`    body {`);
+        lines.push(`        margin: 0;`);
+        lines.push(`    }`);
+        lines.push(`    canvas {`);
+        lines.push(`        max-width: calc(100% - 40px);`);
+        lines.push(`        max-height: calc(100% - 40px);`);
+        lines.push(`        padding: 20px;`);
+        lines.push(`    }`);
+        lines.push(`</style>`);
+        lines.push(`</head>`);
+        lines.push(`<body>`);
+        lines.push(`<script>`);
+        lines.push(`"use strict";`);
+
+        lines.push(``);
+
+        lines.push(`let promises = [];`);
+        lines.push(`let objects = {};`);
+
+        lines.push(``);
+
+        lines.push(`let canvas = document.body.appendChild(document.createElement("canvas"));`);
+        for (let [attribute, value] of Object.entries(this._initialState.attributes))
+            lines.push(`canvas.${attribute} = ${JSON.stringify(value)};`);
+
+        lines.push(``);
+
+        let parametersString = this._initialState.parameters.map(processValue).join(`, `);
+        lines.push(`let context = canvas.getContext("2d"${parametersString ? ", " + parametersString : ""});`);
+
+        lines.push(``);
+
+        lines.push(`let frames = [`);
+
+        lines.push(`    function initialState() {`);
+        if (this._initialState.content) {
+            let image = new Image;
+            image.__data = this._initialState.content;
+            lines.push(`        context.drawImage(${processObject(image)}, 0, 0);`);
+            lines.push(``);
+        }
+        for (let state of this._actions[0].states) {
+            for (let [name, value] of state) {
+                if (name === "getPath" || name === "currentX" || name === "currentY")
+                    continue;
+
+                let contextString = `context`;
+                if (name === "setPath") {
+                    lines.push(`        if (${JSON.stringify(name)} in context)`);
+                    contextString = `    ` + contextString;
+                }
+
+                let callString = ``;
+                if (WI.RecordingAction.isFunctionForType(this._type, name))
+                    callString = `(` + value.map(processValue).join(`, `) + `)`;
+                else
+                    callString = ` = ${processValue(value)}`;
+
+                lines.push(`        ${contextString}.${name}${callString};`);
+            }
+
+            if (state !== this._actions[0].states.lastValue) {
+                lines.push(`        context.save();`);
+                lines.push(``);
+            }
+        }
+        lines.push(`    },`);
+
+        lines.push(`    function startRecording() {`);
+        lines.push(`        if (typeof console.record === "function")`);
+        lines.push(`            console.record(context, {name: ${JSON.stringify(this._displayName)}});`);
+        lines.push(`    },`);
+
+        for (let i = 0; i < this._frames.length; ++i) {
+            lines.push(`    function frame${i + 1}() {`);
+
+            for (let action of this._frames[i].actions) {
+                let contextString = `context`;
+                if (action.contextReplacer)
+                    contextString += `.${action.contextReplacer}`;
+
+                if (!action.valid)
+                    contextString = `// ` + contextString;
+
+                let callString = ``;
+                if (action.isFunction)
+                    callString += `(` + action.parameters.map(processValue).join(`, `) + `)`;
+                else if (!action.isGetter)
+                    callString += ` = ` + processValue(action.parameters[0]);
+
+                lines.push(`        ${contextString}.${action.name}${callString};`);
+            }
+
+            lines.push(`    },`);
+        }
+
+        lines.push(`    function stopRecording() {`);
+        lines.push(`        if (typeof console.recordEnd === "function")`);
+        lines.push(`            console.recordEnd(context);`);
+        lines.push(`    },`);
+
+        lines.push(`];`);
+
+        lines.push(``);
+
+        if (objects.length) {
+            if (objects.some(({object}) => object instanceof CanvasGradient)) {
+                lines.push(`function rebuildCanvasGradient(key, data) {`);
+                lines.push(`    let gradient = null;`);
+                lines.push(`    if (data.type === "radial-gradient")`);
+                lines.push(`        gradient = context.createRadialGradient(data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]);`);
+                lines.push(`    else`);
+                lines.push(`        gradient = context.createLinearGradient(data.points[0], data.points[1], data.points[2], data.points[3]);`);
+                lines.push(`    for (let stop of data.stops)`);
+                lines.push(`        gradient.addColorStop(stop.offset, stop.color);`);
+                lines.push(`    objects[key] = gradient;`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof CanvasPattern)) {
+                lines.push(`function rebuildCanvasPattern(key, data) {`);
+                lines.push(`    promises.push(new Promise(function(resolve, reject) {`);
+                lines.push(`        let image = new Image;`);
+                lines.push(`        function resolveWithImage(event) {`);
+                lines.push(`            objects[key] = context.createPattern(image, data.repeat);`);
+                lines.push(`            resolve();`);
+                lines.push(`        }`);
+                lines.push(`        image.addEventListener("load", resolveWithImage);`);
+                lines.push(`        image.addEventListener("error", resolveWithImage);`);
+                lines.push(`        image.src = ""
+                lines.push(`    }));`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof DOMMatrix)) {
+                lines.push(`function rebuildDOMMatrix(key, data) {`);
+                lines.push(`    objects[key] = new DOMMatrix(data);`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof Image)) {
+                lines.push(`function rebuildImage(key, data) {`);
+                lines.push(`    promises.push(new Promise(function(resolve, reject) {`);
+                lines.push(`        let image = new Image;`);
+                lines.push(`        function resolveWithImage(event) {`);
+                lines.push(`            objects[key] = image;`);
+                lines.push(`            resolve();`);
+                lines.push(`        }`);
+                lines.push(`        image.addEventListener("load", resolveWithImage);`);
+                lines.push(`        image.addEventListener("error", resolveWithImage);`);
+                lines.push(`        image.src = ""
+                lines.push(`    }));`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof ImageBitmap)) {
+                lines.push(`function rebuildImageBitmap(key, data) {`);
+                lines.push(`    promises.push(new Promise(function(resolve, reject) {`);
+                lines.push(`        let image = new Image;`);
+                lines.push(`        function resolveWithImage(event) {`);
+                lines.push(`            createImageBitmap(image).then(function(imageBitmap) {`);
+                lines.push(`                objects[key] = imageBitmap;`);
+                lines.push(`                resolve();`);
+                lines.push(`            });`);
+                lines.push(`        }`);
+                lines.push(`        image.addEventListener("load", resolveWithImage);`);
+                lines.push(`        image.addEventListener("error", resolveWithImage);`);
+                lines.push(`        image.src = ""
+                lines.push(`    }));`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof ImageData)) {
+                lines.push(`function rebuildImageData(key, data) {`);
+                lines.push(`    objects[key] = new ImageData(new Uint8ClampedArray(data.data), parseInt(data.width), parseInt(data.height));`);
+                lines.push(`}`);
+            }
+
+            if (objects.some(({object}) => object instanceof Path2D)) {
+                lines.push(`function rebuildPath2D(key, data) {`);
+                lines.push(`    objects[key] = new Path2D(data);`);
+                lines.push(`}`);
+            }
+
+            lines.push(``);
+
+            for (let {object, index} of objects) {
+                if (object instanceof CanvasGradient) {
+                    lines.push(`rebuildCanvasGradient(${index}, ${JSON.stringify(object.__data)});`);
+                    continue;
+                }
+
+                if (object instanceof CanvasPattern) {
+                    lines.push(`rebuildCanvasPattern(${index}, ${JSON.stringify(object.__data)});`);
+                    continue;
+                }
+
+                if (object instanceof DOMMatrix) {
+                    lines.push(`rebuildDOMMatrix(${index}, ${JSON.stringify(object.toString())});`)
+                    continue;
+                }
+
+                if (object instanceof Image) {
+                    lines.push(`rebuildImage(${index}, ${JSON.stringify(object.__data)});`)
+                    continue;
+                }
+
+                if (object instanceof ImageBitmap) {
+                    lines.push(`rebuildImageBitmap(${index}, ${JSON.stringify(object.__data)});`)
+                    continue;
+                }
+
+                if (object instanceof ImageData) {
+                    lines.push(`rebuildImageData(${index}, ${JSON.stringify(object.__data)});`);
+                    continue;
+                }
+
+                if (object instanceof Path2D) {
+                    lines.push(`rebuildPath2D(${index}, ${JSON.stringify(object.__data || "")});`)
+                    continue;
+                }
+            }
+
+            lines.push(``);
+        }
+
+        lines.push(`Promise.all(promises).then(function() {`);
+        lines.push(`    window.requestAnimationFrame(function executeFrame() {`);
+        lines.push(`        frames.shift()();`);
+        lines.push(`        if (frames.length)`);
+        lines.push(`            window.requestAnimationFrame(executeFrame);`);
+        lines.push(`    });`);
+        lines.push(`});`);
+
+        lines.push(`</script>`);
+        lines.push(`</body>`);
+        return lines.join(`\n`);
+    }
+
     // Private
 
     async _process()

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/CanvasOverviewContentView.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -63,7 +63,7 @@
         }
 
         this._importButtonNavigationItem = new WI.ButtonNavigationItem("import-recording", WI.UIString("Import"), "Images/Import.svg", 15, 15);
-        this._importButtonNavigationItem.toolTip = WI.UIString("Import");
+        this._importButtonNavigationItem.tooltip = WI.UIString("Import");
         this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
 
         this._refreshButtonNavigationItem = new WI.ButtonNavigationItem("refresh-all", WI.UIString("Refresh all"), "Images/ReloadFull.svg", 13, 13);

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/HeapAllocationsTimelineView.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -60,7 +60,7 @@
         };
 
         this._importButtonNavigationItem = new WI.ButtonNavigationItem("import", WI.UIString("Import"), "Images/Import.svg", 15, 15);
-        this._importButtonNavigationItem.toolTip = WI.UIString("Import");
+        this._importButtonNavigationItem.tooltip = WI.UIString("Import");
         this._importButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
         this._importButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._importButtonNavigationItemClicked, this);
 

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotContentView.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotContentView.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/HeapSnapshotContentView.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -34,7 +34,7 @@
         this.element.classList.add("heap-snapshot");
 
         this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export", WI.UIString("Export"), "Images/Export.svg", 15, 15);
-        this._exportButtonNavigationItem.toolTip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName);
+        this._exportButtonNavigationItem.tooltip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName);
         this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
         this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
         this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportSnapshot(); });

Modified: trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js (242808 => 242809)


--- trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js	2019-03-12 18:56:28 UTC (rev 242808)
+++ trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js	2019-03-12 18:59:29 UTC (rev 242809)
@@ -62,10 +62,10 @@
             this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value;
 
             this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export-recording", WI.UIString("Export"), "Images/Export.svg", 15, 15);
-            this._exportButtonNavigationItem.toolTip = WI.UIString("Export recording (%s)").format(WI.saveKeyboardShortcut.displayName);
             this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText;
             this._exportButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High;
-            this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, () => { this._exportRecording(); });
+            this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleExportNavigationItemClicked, this);
+            this._updateExportButton();
         }
     }
 
@@ -175,17 +175,29 @@
 
     _exportRecording()
     {
-        if (!this.representedObject) {
+        let filename = this.representedObject.displayName;
+        let url = "" + encodeURI(filename) + ".json";
+
+        WI.FileUtilities.save({
+            url,
+            content: JSON.stringify(this.representedObject.toJSON()),
+            forceSaveAs: true,
+        });
+    }
+
+    _exportReduction()
+    {
+        if (!this.representedObject.ready) {
             InspectorFrontendHost.beep();
             return;
         }
 
         let filename = this.representedObject.displayName;
-        let url = "" + encodeURI(filename) + ".json";
+        let url = "" + encodeURI(filename) + ".html";
 
         WI.FileUtilities.save({
             url,
-            content: JSON.stringify(this.representedObject.toJSON()),
+            content: this.representedObject.toHTML(),
             forceSaveAs: true,
         });
     }
@@ -423,6 +435,14 @@
         }
     }
 
+    _updateExportButton()
+    {
+        if (this.representedObject.type === WI.Recording.Type.Canvas2D && this.representedObject.ready)
+            this._exportButtonNavigationItem.tooltip = WI.UIString("Export recording (%s)\nShift-click to export a HTML reduction").format(WI.saveKeyboardShortcut.displayName);
+        else
+            this._exportButtonNavigationItem.tooltip = WI.UIString("Export recording (%s)").format(WI.saveKeyboardShortcut.displayName);
+    }
+
     _updateCanvasPath()
     {
         let activated = WI.settings.showCanvasPath.value;
@@ -472,6 +492,14 @@
         this._updateImageGrid();
     }
 
+    _handleExportNavigationItemClicked(event)
+    {
+        if (event.data.nativeEvent.shiftKey && this.representedObject.type === WI.Recording.Type.Canvas2D && this.representedObject.ready)
+            this._exportReduction();
+        else
+            this._exportRecording();
+    }
+
     _sliderChanged()
     {
         let index = 0;
@@ -485,6 +513,8 @@
 
     _handleRecordingProcessedAction(event)
     {
+        this._updateExportButton();
+
         this._sliderElement.max = this.representedObject.visualActionIndexes.length;
         this._updateSliderValue();
 
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to