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();