Web Inspector: Canvas: capture changes to <canvas> that would affect the recorded...
authordrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 3 Nov 2018 23:24:35 +0000 (23:24 +0000)
committerdrousso@apple.com <drousso@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Sat, 3 Nov 2018 23:24:35 +0000 (23:24 +0000)
https://bugs.webkit.org/show_bug.cgi?id=190854

Reviewed by Matt Baker.

Source/WebCore:

Updated existing tests: inspector/canvas/recording-2d.html
                        inspector/canvas/recording-bitmaprenderer.html
                        inspector/canvas/recording-webgl.html

* html/HTMLCanvasElement.idl:
Apply `CallTracingCallback=recordCanvasAction` to the `width` and `height` attributes so
that they are recorded through the same path as `CanvasRenderingContext`.

* html/CanvasBase.h:
* html/CanvasBase.cpp:
(WebCore::CanvasBase::callTracingActive const): Added.

* bindings/js/CallTracer.h:
* bindings/js/CallTracer.cpp:
(WebCore::CallTracer::recordCanvasAction):

Source/WebInspectorUI:

* UserInterface/Models/RecordingAction.js:
(WI.RecordingAction):
(WI.RecordingAction.isFunctionForType):
(WI.RecordingAction.constantNameForParameter):
(WI.RecordingAction.prototype.get contextReplacer): Added.
(WI.RecordingAction.prototype.async.swizzle):
(WI.RecordingAction.prototype.apply):
Create a constant list of actions for each recording type that need to replace the context
with a different value before being applied (e.g. `width` should be applied to the
`context`'s `canvas` instead of directly to the `context`).

* UserInterface/Views/RecordingContentView.js:
(WI.RecordingContentView.prototype._generateContentCanvas2D.actionModifiesPath): Added.
(WI.RecordingContentView.prototype._generateContentCanvas2D):
(WI.RecordingContentView._actionModifiesPath): Deleted.
Generate the path context after the actions are applied to the preview context so that the
final width/height are known and can be used. This is needed because changing the
width/height causes the content to be erased.

* UserInterface/Views/RecordingActionTreeElement.js:
(WI.RecordingActionTreeElement._generateDOM):
(WI.RecordingActionTreeElement._classNameForAction):
* UserInterface/Views/RecordingActionTreeElement.css:
(.tree-outline:focus .item.action.selected:not(.invalid, .initial-state, .has-context-replacer) > .icon): Added.
(.item.action > .titles .context-replacer::after): Added.
(.item.action.has-context-replacer > .icon): Added.
(@media (prefers-dark-interface) .item.action:not(.invalid, .initial-state, .has-context-replacer) > .icon): Added.
(.tree-outline:focus .item.action.selected:not(.initial-state, .invalid) > .icon): Deleted.
(@media (prefers-dark-interface) .item.action:not(.initial-state) > .icon): Deleted.
(@media (prefers-dark-interface) .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.initial-state, .parent) > .icon): Deleted.
Add the context replacer text to the beginning of the action's name if it exists.

* UserInterface/Views/CanvasContentView.js:
(WI.CanvasContentView.prototype._refreshPixelSize):
(WI.CanvasContentView.prototype._updatePixelSize): Deleted.
Update preview image when the canvas' size changes.

LayoutTests:

* inspector/canvas/recording-2d-expected.txt:
* inspector/canvas/recording-2d.html:
* inspector/canvas/recording-bitmaprenderer-expected.txt:
* inspector/canvas/recording-bitmaprenderer.html:
* inspector/canvas/recording-webgl-expected.txt:
* inspector/canvas/recording-webgl.html:

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@237777 268f45cc-cd09-0410-ab3c-d52691b4dbfc

19 files changed:
LayoutTests/ChangeLog
LayoutTests/inspector/canvas/recording-2d-expected.txt
LayoutTests/inspector/canvas/recording-2d.html
LayoutTests/inspector/canvas/recording-bitmaprenderer-expected.txt
LayoutTests/inspector/canvas/recording-bitmaprenderer.html
LayoutTests/inspector/canvas/recording-webgl-expected.txt
LayoutTests/inspector/canvas/recording-webgl.html
Source/WebCore/ChangeLog
Source/WebCore/bindings/js/CallTracer.cpp
Source/WebCore/bindings/js/CallTracer.h
Source/WebCore/html/CanvasBase.cpp
Source/WebCore/html/CanvasBase.h
Source/WebCore/html/HTMLCanvasElement.idl
Source/WebInspectorUI/ChangeLog
Source/WebInspectorUI/UserInterface/Models/RecordingAction.js
Source/WebInspectorUI/UserInterface/Views/CanvasContentView.js
Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.css
Source/WebInspectorUI/UserInterface/Views/RecordingActionTreeElement.js
Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

index 87cbd2a..8189be4 100644 (file)
@@ -1,3 +1,17 @@
+2018-11-03  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Canvas: capture changes to <canvas> that would affect the recorded context
+        https://bugs.webkit.org/show_bug.cgi?id=190854
+
+        Reviewed by Matt Baker.
+
+        * inspector/canvas/recording-2d-expected.txt:
+        * inspector/canvas/recording-2d.html:
+        * inspector/canvas/recording-bitmaprenderer-expected.txt:
+        * inspector/canvas/recording-bitmaprenderer.html:
+        * inspector/canvas/recording-webgl-expected.txt:
+        * inspector/canvas/recording-webgl.html:
+
 2018-11-03  Andy Estes  <aestes@apple.com>
 
         [Payment Request] PaymentResponse.retry()'s errorFields should be optional
index b53c3f6..082f21d 100644 (file)
@@ -1067,6 +1067,26 @@ frames:
       trace:
         0: (anonymous function)
         1: executeFrameFunction
+  73: (duration)
+    0: width
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: width = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+  74: (duration)
+    0: height
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: height = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
 
 -- Running test case: Canvas.recording2D.memoryLimit
 initialState:
index 7f04822..e8066ef 100644 (file)
@@ -384,6 +384,14 @@ function performActions() {
             ctx.webkitLineDashOffset = 1;
         },
         () => {
+            ctx.canvas.width;
+            ctx.canvas.width = 2;
+        },
+        () => {
+            ctx.canvas.height;
+            ctx.canvas.height = 2;
+        },
+        () => {
             TestPage.dispatchEventToFrontend("LastFrame");
         },
     ];
index d2b4532..d40088d 100644 (file)
@@ -43,6 +43,26 @@ frames:
         5: (anonymous function)
         6: promiseReactionJob
       snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAABNJREFUCB1j/M/AAEQMDEwgAgQAHxcCAmtAm/sAAAAASUVORK5CYII="
+  1: (duration)
+    0: width
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: width = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+  2: (duration)
+    0: height
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: height = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
 
 -- Running test case: Canvas.recordingBitmapRenderer.memoryLimit
 initialState:
index 0321d0c..3b0bff4 100644 (file)
@@ -53,6 +53,14 @@ async function performActions() {
             ctx.transferFromImageBitmap(redBitmap);
         },
         () => {
+            ctx.canvas.width;
+            ctx.canvas.width = 2;
+        },
+        () => {
+            ctx.canvas.height;
+            ctx.canvas.height = 2;
+        },
+        () => {
             TestPage.dispatchEventToFrontend("LastFrame");
         },
     ];
index 6312e37..d8bc49c 100644 (file)
@@ -5,11 +5,11 @@ Test that CanvasManager is able to record actions made to WebGL canvas contexts.
 -- Running test case: Canvas.recordingWebGL.singleFrame
 initialState:
   attributes:
-    width: 300
-    height: 150
+    width: 2
+    height: 2
   parameters:
     0: {"alpha":true,"depth":true,"stencil":false,"antialias":true,"premultipliedAlpha":true,"preserveDrawingBuffer":false,"failIfMajorPerformanceCaveat":false}
-  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
 frames:
   0: (duration)
     0: activeTexture(1)
@@ -27,11 +27,11 @@ frames:
 -- Running test case: Canvas.recordingWebGL.multipleFrames
 initialState:
   attributes:
-    width: 300
-    height: 150
+    width: 2
+    height: 2
   parameters:
     0: {"alpha":true,"depth":true,"stencil":false,"antialias":true,"premultipliedAlpha":true,"preserveDrawingBuffer":false,"failIfMajorPerformanceCaveat":false}
-  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
 frames:
   0: (duration)
     0: activeTexture(1)
@@ -156,7 +156,7 @@ frames:
         0: clear
         1: (anonymous function)
         2: executeFrameFunction
-      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
   16: (duration)
     0: clearColor(1, 2, 3, 4)
       swizzleTypes: [Number, Number, Number, Number]
@@ -355,7 +355,7 @@ frames:
         0: drawArrays
         1: (anonymous function)
         2: executeFrameFunction
-      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
   45: (duration)
     0: drawElements(1, 2, 3, 4)
       swizzleTypes: [Number, Number, Number, Number]
@@ -363,7 +363,7 @@ frames:
         0: drawElements
         1: (anonymous function)
         2: executeFrameFunction
-      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+      snapshot: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
   46: (duration)
     0: enable(1)
       swizzleTypes: [Number]
@@ -1006,15 +1006,35 @@ frames:
         0: viewport
         1: (anonymous function)
         2: executeFrameFunction
+  137: (duration)
+    0: width
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: width = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+  138: (duration)
+    0: height
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
+    1: height = 2
+      swizzleTypes: [Number]
+      trace:
+        0: (anonymous function)
+        1: executeFrameFunction
 
 -- Running test case: Canvas.recordingWebGL.memoryLimit
 initialState:
   attributes:
-    width: 300
-    height: 150
+    width: 2
+    height: 2
   parameters:
     0: {"alpha":true,"depth":true,"stencil":false,"antialias":true,"premultipliedAlpha":true,"preserveDrawingBuffer":false,"failIfMajorPerformanceCaveat":false}
-  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAACWCAYAAABkW7XSAAAAAXNSR0IArs4c6QAAAylJREFUeAHt0DEBAAAAwqD1T20IX4hAYcCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYMCAAQMGDBgwYOAdGL/UAAEPpnR6AAAAAElFTkSuQmCC"
+  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
 frames:
   0: (duration) (incomplete)
     0: activeTexture(1)
index 3703676..5867438 100644 (file)
@@ -39,6 +39,9 @@ function load() {
     createProgram("webgl");
     linkProgram("vertex-shader", "fragment-shader");
 
+    context.canvas.width = 2;
+    context.canvas.height = 2;
+
     buffer = context.createBuffer();
     framebuffer = context.createFramebuffer();
     uniformLocation = context.getUniformLocation(program, "test");
@@ -484,6 +487,14 @@ function performActions() {
             context.viewport(1, 2, 3, 4);
         },
         () => {
+            context.canvas.width;
+            context.canvas.width = 2;
+        },
+        () => {
+            context.canvas.height;
+            context.canvas.height = 2;
+        },
+        () => {
             TestPage.dispatchEventToFrontend("LastFrame");
         },
     ];
index e9818ae..9a5c19c 100644 (file)
@@ -1,3 +1,27 @@
+2018-11-03  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Canvas: capture changes to <canvas> that would affect the recorded context
+        https://bugs.webkit.org/show_bug.cgi?id=190854
+
+        Reviewed by Matt Baker.
+
+        Updated existing tests: inspector/canvas/recording-2d.html
+                                inspector/canvas/recording-bitmaprenderer.html
+                                inspector/canvas/recording-webgl.html
+
+        * html/HTMLCanvasElement.idl:
+        Apply `CallTracingCallback=recordCanvasAction` to the `width` and `height` attributes so
+        that they are recorded through the same path as `CanvasRenderingContext`.
+
+        * html/CanvasBase.h:
+        * html/CanvasBase.cpp:
+        (WebCore::CanvasBase::callTracingActive const): Added.
+
+        * bindings/js/CallTracer.h:
+        * bindings/js/CallTracer.cpp:
+        (WebCore::CallTracer::recordCanvasAction):
+
+
 2018-11-03  Andy Estes  <aestes@apple.com>
 
         [Payment Request] PaymentResponse.retry()'s errorFields should be optional
index 4de19c9..807116d 100644 (file)
 #include "CallTracer.h"
 
 #include "CanvasRenderingContext.h"
+#include "HTMLCanvasElement.h"
 #include "InspectorInstrumentation.h"
 
 namespace WebCore {
 
+void CallTracer::recordCanvasAction(const HTMLCanvasElement& canvasElement, const String& name, Vector<RecordCanvasActionVariant>&& parameters)
+{
+    if (auto* canvasRenderingContext = canvasElement.renderingContext())
+        InspectorInstrumentation::recordCanvasAction(*canvasRenderingContext, name, WTFMove(parameters));
+}
+
 void CallTracer::recordCanvasAction(CanvasRenderingContext& canvasRenderingContext, const String& name, Vector<RecordCanvasActionVariant>&& parameters)
 {
     InspectorInstrumentation::recordCanvasAction(canvasRenderingContext, name, WTFMove(parameters));
index 9e7f23b..db264f3 100644 (file)
 namespace WebCore {
 
 class CanvasRenderingContext;
+class HTMLCanvasElement;
 
 class CallTracer {
 public:
+    static void recordCanvasAction(const HTMLCanvasElement&, const String&, Vector<RecordCanvasActionVariant>&& = { });
     static void recordCanvasAction(CanvasRenderingContext&, const String&, Vector<RecordCanvasActionVariant>&& = { });
 };
 
index b8b471e..a8ae80a 100644 (file)
@@ -109,4 +109,9 @@ HashSet<Element*> CanvasBase::cssCanvasClients() const
     return cssCanvasClients;
 }
 
+bool CanvasBase::callTracingActive() const
+{
+    return m_context && m_context->callTracingActive();
+}
+
 }
index c5423b1..f9409d1 100644 (file)
@@ -95,6 +95,8 @@ public:
     virtual AffineTransform baseTransform() const = 0;
     virtual Image* copiedImage() const = 0;
 
+    bool callTracingActive() const;
+
 protected:
     CanvasBase(ScriptExecutionContext*);
 
index 43546e9..c0a4043 100644 (file)
@@ -45,8 +45,8 @@ typedef (
     ReportExtraMemoryCost,
     ReportExternalMemoryCost,
 ] interface HTMLCanvasElement : HTMLElement {
-    attribute unsigned long width;
-    attribute unsigned long height;
+    [CallTracingCallback=recordCanvasAction] attribute unsigned long width;
+    [CallTracingCallback=recordCanvasAction] attribute unsigned long height;
 
     [CallWith=ExecState, MayThrowException] RenderingContext? getContext(DOMString contextId, any... arguments);
 
index b944ecc..76e7f19 100644 (file)
@@ -1,3 +1,47 @@
+2018-11-03  Devin Rousso  <drousso@apple.com>
+
+        Web Inspector: Canvas: capture changes to <canvas> that would affect the recorded context
+        https://bugs.webkit.org/show_bug.cgi?id=190854
+
+        Reviewed by Matt Baker.
+
+        * UserInterface/Models/RecordingAction.js:
+        (WI.RecordingAction):
+        (WI.RecordingAction.isFunctionForType):
+        (WI.RecordingAction.constantNameForParameter):
+        (WI.RecordingAction.prototype.get contextReplacer): Added.
+        (WI.RecordingAction.prototype.async.swizzle):
+        (WI.RecordingAction.prototype.apply):
+        Create a constant list of actions for each recording type that need to replace the context
+        with a different value before being applied (e.g. `width` should be applied to the
+        `context`'s `canvas` instead of directly to the `context`).
+
+        * UserInterface/Views/RecordingContentView.js:
+        (WI.RecordingContentView.prototype._generateContentCanvas2D.actionModifiesPath): Added.
+        (WI.RecordingContentView.prototype._generateContentCanvas2D):
+        (WI.RecordingContentView._actionModifiesPath): Deleted.
+        Generate the path context after the actions are applied to the preview context so that the
+        final width/height are known and can be used. This is needed because changing the
+        width/height causes the content to be erased.
+
+        * UserInterface/Views/RecordingActionTreeElement.js:
+        (WI.RecordingActionTreeElement._generateDOM):
+        (WI.RecordingActionTreeElement._classNameForAction):
+        * UserInterface/Views/RecordingActionTreeElement.css:
+        (.tree-outline:focus .item.action.selected:not(.invalid, .initial-state, .has-context-replacer) > .icon): Added.
+        (.item.action > .titles .context-replacer::after): Added.
+        (.item.action.has-context-replacer > .icon): Added.
+        (@media (prefers-dark-interface) .item.action:not(.invalid, .initial-state, .has-context-replacer) > .icon): Added.
+        (.tree-outline:focus .item.action.selected:not(.initial-state, .invalid) > .icon): Deleted.
+        (@media (prefers-dark-interface) .item.action:not(.initial-state) > .icon): Deleted.
+        (@media (prefers-dark-interface) .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.initial-state, .parent) > .icon): Deleted.
+        Add the context replacer text to the beginning of the action's name if it exists.
+
+        * UserInterface/Views/CanvasContentView.js:
+        (WI.CanvasContentView.prototype._refreshPixelSize):
+        (WI.CanvasContentView.prototype._updatePixelSize): Deleted.
+        Update preview image when the canvas' size changes.
+
 2018-11-02  Matt Baker  <mattbaker@apple.com>
 
         Web Inspector: support multiple selection/deletion of cookie records
index d186f3f..4d2d7ff 100644 (file)
@@ -45,6 +45,8 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         this._isGetter = false;
         this._isVisual = false;
 
+        this._contextReplacer = null;
+
         this._states = [];
         this._stateModifiers = new Set;
 
@@ -84,7 +86,10 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         let prototype = WI.RecordingAction._prototypeForType(type);
         if (!prototype)
             return false;
-        return typeof Object.getOwnPropertyDescriptor(prototype, name).value === "function";
+        let propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, name);
+        if (!propertyDescriptor)
+            return false;
+        return typeof propertyDescriptor.value === "function";
     }
 
     static constantNameForParameter(type, name, value, index, count)
@@ -123,7 +128,7 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         let prototype = WI.RecordingAction._prototypeForType(type);
         for (let key in prototype) {
             let descriptor = Object.getOwnPropertyDescriptor(prototype, key);
-            if (descriptor.value === value)
+            if (descriptor && descriptor.value === value)
                 return key;
         }
 
@@ -198,6 +203,7 @@ WI.RecordingAction = class RecordingAction extends WI.Object
     get isFunction() { return this._isFunction; }
     get isGetter() { return this._isGetter; }
     get isVisual() { return this._isVisual; }
+    get contextReplacer() { return this._contextReplacer; }
     get states() { return this._states; }
     get stateModifiers() { return this._stateModifiers; }
     get warning() { return this._warning; }
@@ -330,18 +336,31 @@ WI.RecordingAction = class RecordingAction extends WI.Object
         if (this._payloadSnapshot >= 0)
             this._snapshot = snapshot;
 
-        this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
-        this._isGetter = !this._isFunction && !this._parameters.length;
+        if (recording.type === WI.Recording.Type.Canvas2D || recording.type === WI.Recording.Type.CanvasBitmapRenderer || recording.type === WI.Recording.Type.CanvasWebGL) {
+            if (this._name === "width" || this._name === "height") {
+                this._contextReplacer = "canvas";
+                this._isFunction = false;
+                this._isGetter = !this._parameters.length;
+                this._isVisual = !this._isGetter;
+            }
 
-        let visualNames = WI.RecordingAction._visualNames[recording.type];
-        this._isVisual = visualNames ? visualNames.has(this._name) : false;
+            // FIXME: <https://webkit.org/b/180833>
+        }
 
-        if (this._valid) {
-            let prototype = WI.RecordingAction._prototypeForType(recording.type);
-            if (prototype && !(name in prototype)) {
-                this.markInvalid();
+        if (!this._contextReplacer) {
+            this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
+            this._isGetter = !this._isFunction && !this._parameters.length;
 
-                WI.Recording.synthesizeError(WI.UIString("ā€œ%sā€ is invalid.").format(name));
+            let visualNames = WI.RecordingAction._visualNames[recording.type];
+            this._isVisual = visualNames ? visualNames.has(this._name) : false;
+
+            if (this._valid) {
+                let prototype = WI.RecordingAction._prototypeForType(recording.type);
+                if (prototype && !(name in prototype)) {
+                    this.markInvalid();
+
+                    WI.Recording.synthesizeError(WI.UIString("ā€œ%sā€ is invalid.").format(name));
+                }
             }
         }
 
@@ -375,6 +394,10 @@ WI.RecordingAction = class RecordingAction extends WI.Object
 
         try {
             let name = options.nameOverride || this._name;
+
+            if (this._contextReplacer)
+                context = context[this._contextReplacer];
+
             if (this.isFunction)
                 context[name](...this._parameters);
             else {
index 00d4e39..c69b99d 100644 (file)
@@ -287,13 +287,28 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
 
     _refreshPixelSize()
     {
-        this._pixelSize = null;
+        let updatePixelSize = (size) => {
+            if (this._pixelSize && size.width === this._pixelSize.width && size.height === this._pixelSize.height)
+                return;
 
-        this.representedObject.requestSize().then((size) => {
             this._pixelSize = size;
-            this._updatePixelSize();
-        }).catch((error) => {
-            this._updatePixelSize();
+
+            if (this._pixelSizeElement) {
+                if (this._pixelSize)
+                    this._pixelSizeElement.textContent = `${this._pixelSize.width} ${multiplicationSign} ${this._pixelSize.height}`;
+                else
+                    this._pixelSizeElement.textContent = emDash;
+            }
+
+            this.refresh();
+        };
+
+        this.representedObject.requestSize()
+        .then((size) => {
+            updatePixelSize(size);
+        })
+        .catch((error) => {
+            updatePixelSize(null);
         });
     }
 
@@ -349,17 +364,6 @@ WI.CanvasContentView = class CanvasContentView extends WI.ContentView
         }
     }
 
-    _updatePixelSize()
-    {
-        if (!this._pixelSizeElement)
-            return;
-
-        if (this._pixelSize)
-            this._pixelSizeElement.textContent = `${this._pixelSize.width} ${multiplicationSign} ${this._pixelSize.height}`;
-        else
-            this._pixelSizeElement.textContent = emDash;
-    }
-
     _updateRecordNavigationItem()
     {
         if (!this._recordButtonNavigationItem)
index a1ab570..3ef0682 100644 (file)
@@ -52,7 +52,7 @@ body[dir=rtl] .item.action::before {
     margin-left: var(--tree-outline-icon-margin-end);
 }
 
-.tree-outline:focus .item.action.selected:not(.initial-state, .invalid) > .icon {
+.tree-outline:focus .item.action.selected:not(.invalid, .initial-state, .has-context-replacer) > .icon {
     filter: invert();
     opacity: 1;
 }
@@ -109,6 +109,10 @@ body[dir=rtl] .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.init
     background-color: var(--value-changed-highlight);
 }
 
+.item.action > .titles .context-replacer::after {
+    content: ".";
+}
+
 .item.action.attribute > .titles .parameters::before {
     content: " = ";
 }
@@ -133,6 +137,10 @@ body[dir=rtl] .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.init
     vertical-align: -1px;
 }
 
+.item.action.has-context-replacer > .icon {
+    content: url("../Images/Source.svg");
+}
+
 .item.action.composite > .icon {
     content: url(../Images/Composite.svg);
 }
@@ -236,11 +244,7 @@ body[dir=rtl] .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.init
         color: var(--green-highlight-text-color);
     }
 
-    .item.action:not(.initial-state) > .icon {
-        filter: invert();
-    }
-
-    .tree-outline:not(.hide-disclosure-buttons) .item.action:not(.initial-state, .parent) > .icon {
+    .item.action:not(.invalid, .initial-state, .has-context-replacer) > .icon {
         filter: invert();
         opacity: 0.8;
     }
index 5c8c4d7..dbadc7d 100644 (file)
@@ -105,6 +105,15 @@ WI.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.Gene
         let titleFragment = document.createDocumentFragment();
         let copyText = recordingAction.name;
 
+        let contextReplacer = recordingAction.contextReplacer;
+        if (contextReplacer) {
+            copyText = contextReplacer + "." + copyText;
+
+            let contextReplacerContainer = titleFragment.appendChild(document.createElement("span"));
+            contextReplacerContainer.classList.add("context-replacer");
+            contextReplacerContainer.textContent = contextReplacer;
+        }
+
         let nameContainer = titleFragment.appendChild(document.createElement("span"));
         nameContainer.classList.add("name");
         nameContainer.textContent = recordingAction.name;
@@ -247,6 +256,9 @@ WI.RecordingActionTreeElement = class RecordingActionTreeElement extends WI.Gene
 
     static _classNameForAction(recordingAction)
     {
+        if (recordingAction.contextReplacer)
+            return "has-context-replacer";
+
         function classNameForActionName(name) {
             switch (name) {
             case "arc":
index 344d7e3..f6096c5 100644 (file)
@@ -64,27 +64,6 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
         }
     }
 
-    // Static
-
-    static _actionModifiesPath(recordingAction)
-    {
-        switch (recordingAction.name) {
-        case "arc":
-        case "arcTo":
-        case "beginPath":
-        case "bezierCurveTo":
-        case "closePath":
-        case "ellipse":
-        case "lineTo":
-        case "moveTo":
-        case "quadraticCurveTo":
-        case "rect":
-            return true;
-        }
-
-        return false;
-    }
-
     // Public
 
     get navigationItems()
@@ -241,6 +220,9 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             let saveCount = 0;
             snapshot.context.save();
 
+            for (let attribute in snapshot.attributes)
+                snapshot.element[attribute] = snapshot.attributes[attribute];
+
             if (snapshot.content) {
                 snapshot.context.clearRect(0, 0, snapshot.element.width, snapshot.element.height);
                 snapshot.context.drawImage(snapshot.content, 0, 0);
@@ -269,8 +251,21 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
                 snapshot.context.save();
             }
 
-            let shouldDrawCanvasPath = showCanvasPath && indexOfLastBeginPathAction <= to;
-            if (shouldDrawCanvasPath) {
+            let lastPathPoint = {};
+            let subPathStartPoint = {};
+
+            for (let i = from; i <= to; ++i) {
+                if (actions[i].name === "save")
+                    ++saveCount;
+                else if (actions[i].name === "restore") {
+                    if (!saveCount) // Only attempt to restore if save has been called.
+                        continue;
+                }
+
+                actions[i].apply(snapshot.context);
+            }
+
+            if (showCanvasPath && indexOfLastBeginPathAction <= to) {
                 if (!this._pathContext) {
                     let pathCanvas = document.createElement("canvas");
                     pathCanvas.classList.add("path");
@@ -285,22 +280,29 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
                 this._pathContext.fillStyle = "hsla(0, 0%, 100%, 0.75)";
                 this._pathContext.fillRect(0, 0, snapshot.element.width, snapshot.element.height);
-            }
 
-            let lastPathPoint = {};
-            let subPathStartPoint = {};
+                function actionModifiesPath(action) {
+                    switch (action.name) {
+                    case "arc":
+                    case "arcTo":
+                    case "beginPath":
+                    case "bezierCurveTo":
+                    case "closePath":
+                    case "ellipse":
+                    case "lineTo":
+                    case "moveTo":
+                    case "quadraticCurveTo":
+                    case "rect":
+                        return true;
+                    }
 
-            for (let i = from; i <= to; ++i) {
-                if (actions[i].name === "save")
-                    ++saveCount;
-                else if (actions[i].name === "restore") {
-                    if (!saveCount) // Only attempt to restore if save has been called.
-                        continue;
+                    return false;
                 }
 
-                actions[i].apply(snapshot.context);
+                for (let i = indexOfLastBeginPathAction; i <= to; ++i) {
+                    if (!actionModifiesPath(actions[i]))
+                        continue;
 
-                if (shouldDrawCanvasPath && i >= indexOfLastBeginPathAction && WI.RecordingContentView._actionModifiesPath(actions[i])) {
                     lastPathPoint = {x: this._pathContext.currentX, y: this._pathContext.currentY};
 
                     if (i === indexOfLastBeginPathAction)
@@ -326,9 +328,7 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
 
                     this._pathContext.stroke();
                 }
-            }
 
-            if (shouldDrawCanvasPath) {
                 this._pathContext.restore();
                 this._previewContainer.appendChild(this._pathContext.canvas);
             } else if (this._pathContext)
@@ -358,10 +358,16 @@ WI.RecordingContentView = class RecordingContentView extends WI.ContentView
             if (lastSnapshotIndex < 0) {
                 snapshot.content = this._initialContent;
                 snapshot.states = actions[0].states;
+                snapshot.attributes = Object.shallowCopy(initialState.attributes);
             } else {
-                snapshot.content = this._snapshots[lastSnapshotIndex].content;
-                snapshot.states = this._snapshots[lastSnapshotIndex].states;
-                startIndex = this._snapshots[lastSnapshotIndex].index;
+                let lastSnapshot = this._snapshots[lastSnapshotIndex];
+                snapshot.content = lastSnapshot.content;
+                snapshot.states = lastSnapshot.states;
+                snapshot.attributes = {};
+                for (let attribute in initialState.attributes)
+                    snapshot.attributes[attribute] = lastSnapshot.element[attribute];
+
+                startIndex = lastSnapshot.index;
             }
 
             applyActions(startIndex, snapshot.index - 1);