Changeset 237198 in webkit


Ignore:
Timestamp:
Oct 16, 2018 12:23:21 PM (6 years ago)
Author:
Devin Rousso
Message:

Web Inspector: Canvas: capture previously saved states and add them to the recording payload
https://bugs.webkit.org/show_bug.cgi?id=190473

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

  • inspector/protocol/Recording.json:

Add states key to InitialState object.

Source/WebCore:

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

inspector/model/recording.html

Instead of sending a single object of the current state of the context, send an array of
objects, one for each restore point.

  • html/canvas/CanvasRenderingContext2DBase.h:
  • html/canvas/CanvasRenderingContext2DBase.cpp:

(WebCore::CanvasRenderingContext2DBase::stateStack): Added.

  • inspector/InspectorCanvas.h:
  • inspector/InspectorCanvas.cpp:

(WebCore::InspectorCanvas::stringIndexForKey): Added.
(WebCore::InspectorCanvas::buildInitialState):

Source/WebInspectorUI:

Instead of sending a single object of the current state of the context, send an array of
objects, one for each restore point. When replaying, recreate each restore point before
applying the selected action(s).

  • UserInterface/Models/Recording.js:

(WI.Recording):
(WI.Recording.fromPayload):
(WI.Recording.prototype.toJSON):
(WI.Recording.prototype.async._process):
(WI.Recording.prototype.async._swizzleState): Added.

  • UserInterface/Models/RecordingAction.js:

(WI.RecordingAction):
(WI.RecordingAction.deriveCurrentState): Added.
(WI.RecordingAction.prototype.get states): Added.
(WI.RecordingAction.prototype.process):
(WI.RecordingAction.prototype.get state): Deleted.
Drive-by: when processing, also check to see if any values in the current state changed
outside of those expected in _stateModifiers (e.g. restore may modify some state values).

  • UserInterface/Views/RecordingContentView.js:

(WI.RecordingContentView.prototype._generateContentCanvas2D):

  • UserInterface/Views/RecordingStateDetailsSidebarPanel.js:

(WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
Default to showing the most recent (current) state.

  • UserInterface/Views/CanvasTabContentView.js:

(WI.CanvasTabContentView.prototype.initialLayout): Added.
(WI.CanvasTabContentView.prototype._addCanvas):
(WI.CanvasTabContentView.prototype._removeCanvas):
(WI.CanvasTabContentView.prototype._addRecording): Added.
(WI.CanvasTabContentView.prototype._recordingImportedOrStopped):
(WI.CanvasTabContentView.prototype._recordingAdded): Deleted.

  • UserInterface/Controllers/CanvasManager.js:

(WI.CanvasManager):
(WI.CanvasManager.prototype.get importedRecordings): Added.
(WI.CanvasManager.prototype.importRecording):
Drive-by: store imported recordings on WI.CanvasManager so that if the Canvas tab is
closed we can still show the list of imported recordings.

LayoutTests:

  • inspector/canvas/recording-2d-expected.txt:
  • inspector/canvas/recording-2d.html:
  • inspector/canvas/resources/recording-utilities.js:

(TestPage.registerInitializer):
(TestPage.registerInitializer.async.logRecording): Added.
(TestPage.registerInitializer.logRecording): Deleted.

  • inspector/model/recording-expected.txt:
  • inspector/model/recording.html:
Location:
trunk
Files:
20 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r237192 r237198  
     12018-10-16  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: Canvas: capture previously saved states and add them to the recording payload
     4        https://bugs.webkit.org/show_bug.cgi?id=190473
     5
     6        Reviewed by Joseph Pecoraro.
     7
     8        * inspector/canvas/recording-2d-expected.txt:
     9        * inspector/canvas/recording-2d.html:
     10        * inspector/canvas/resources/recording-utilities.js:
     11        (TestPage.registerInitializer):
     12        (TestPage.registerInitializer.async.logRecording): Added.
     13        (TestPage.registerInitializer.logRecording): Deleted.
     14        * inspector/model/recording-expected.txt:
     15        * inspector/model/recording.html:
     16
    1172018-10-16  Justin Michaud  <justin_michaud@apple.com>
    218
  • trunk/LayoutTests/inspector/canvas/recording-2d-expected.txt

    r236008 r237198  
    88    width: 2
    99    height: 2
    10     setTransform: [1,0,0,1,0,0]
     10  current state:
     11    setTransform: [[1,0,0,1,0,0]]
    1112    globalAlpha: 1
    12     globalCompositeOperation: 0
     13    globalCompositeOperation: "source-over"
    1314    lineWidth: 1
    14     lineCap: 1
    15     lineJoin: 2
     15    lineCap: "butt"
     16    lineJoin: "miter"
    1617    miterLimit: 10
    1718    shadowOffsetX: 0
    1819    shadowOffsetY: 0
    1920    shadowBlur: 0
    20     shadowColor: 3
     21    shadowColor: "rgba(0, 0, 0, 0)"
    2122    setLineDash: [[]]
    2223    lineDashOffset: 0
    23     font: 4
    24     textAlign: 5
    25     textBaseline: 6
    26     direction: 7
    27     strokeStyle: 8
    28     fillStyle: 8
     24    font: "10px sans-serif"
     25    textAlign: "start"
     26    textBaseline: "alphabetic"
     27    direction: "ltr"
     28    strokeStyle: "#000000"
     29    fillStyle: "#000000"
    2930    imageSmoothingEnabled: true
    30     imageSmoothingQuality: 9
    31     setPath: [10]
     31    imageSmoothingQuality: "low"
     32    setPath: [{}]
    3233  parameters:
    3334  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
     
    6667    width: 2
    6768    height: 2
    68     setTransform: [1,0,0,1,0,0]
     69  current state:
     70    setTransform: [[1,0,0,1,0,0]]
    6971    globalAlpha: 1
    70     globalCompositeOperation: 0
     72    globalCompositeOperation: "source-over"
    7173    lineWidth: 1
    72     lineCap: 1
    73     lineJoin: 2
     74    lineCap: "butt"
     75    lineJoin: "miter"
    7476    miterLimit: 10
    7577    shadowOffsetX: 0
    7678    shadowOffsetY: 0
    7779    shadowBlur: 0
    78     shadowColor: 3
     80    shadowColor: "rgba(0, 0, 0, 0)"
    7981    setLineDash: [[]]
    8082    lineDashOffset: 0
    81     font: 4
    82     textAlign: 5
    83     textBaseline: 6
    84     direction: 7
    85     strokeStyle: 8
    86     fillStyle: 8
     83    font: "10px sans-serif"
     84    textAlign: "start"
     85    textBaseline: "alphabetic"
     86    direction: "ltr"
     87    strokeStyle: "#000000"
     88    fillStyle: "#000000"
    8789    imageSmoothingEnabled: true
    88     imageSmoothingQuality: 9
    89     setPath: [10]
     90    imageSmoothingQuality: "low"
     91    setPath: [{}]
    9092  parameters:
    9193  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
     
    10721074    width: 2
    10731075    height: 2
    1074     setTransform: [1,0,0,1,0,0]
     1076  current state:
     1077    setTransform: [[1,0,0,1,0,0]]
    10751078    globalAlpha: 1
    1076     globalCompositeOperation: 0
     1079    globalCompositeOperation: "source-over"
    10771080    lineWidth: 1
    1078     lineCap: 1
    1079     lineJoin: 2
     1081    lineCap: "butt"
     1082    lineJoin: "miter"
    10801083    miterLimit: 10
    10811084    shadowOffsetX: 0
    10821085    shadowOffsetY: 0
    10831086    shadowBlur: 0
    1084     shadowColor: 3
     1087    shadowColor: "rgba(0, 0, 0, 0)"
    10851088    setLineDash: [[]]
    10861089    lineDashOffset: 0
    1087     font: 4
    1088     textAlign: 5
    1089     textBaseline: 6
    1090     direction: 7
    1091     strokeStyle: 8
    1092     fillStyle: 8
     1090    font: "10px sans-serif"
     1091    textAlign: "start"
     1092    textBaseline: "alphabetic"
     1093    direction: "ltr"
     1094    strokeStyle: "#000000"
     1095    fillStyle: "#000000"
    10931096    imageSmoothingEnabled: true
    1094     imageSmoothingQuality: 9
    1095     setPath: [10]
     1097    imageSmoothingQuality: "low"
     1098    setPath: [{}]
    10961099  parameters:
    10971100  content: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAAXNSR0IArs4c6QAAAAtJREFUCB1jYEAHAAASAAGAFMrMAAAAAElFTkSuQmCC"
     
    11231126PASS: The parameter should be null.
    11241127
     1128-- Running test case: Canvas.recording2D.ExistingSaves
     1129PASS: There should be 4 existing states.
     1130PASS: State 0 should match expected fillStyle value.
     1131PASS: State 1 should match expected fillStyle value.
     1132PASS: State 2 should match expected fillStyle value.
     1133PASS: State 3 should match expected fillStyle value.
     1134
    11251135-- Running test case: Canvas.recording2D.NoActions
    11261136PASS: A recording should have been started and stopped once.
  • trunk/LayoutTests/inspector/canvas/recording-2d.html

    r237010 r237198  
    4343
    4444    ctx.save();
    45     ctx.save(); // This matches the `restore` call in `performActions`.
     45    cancelActions();
    4646
    4747    runTest();
     
    5555
    5656let timeoutID = NaN;
    57 let restoreCalled = false;
     57let saveCount = 1;
    5858
    5959function cancelActions() {
    60     if (!isNaN(timeoutID)) {
     60    for (let i = 0; i < saveCount; ++i)
    6161        ctx.restore();
    62         if (!restoreCalled)
    63             ctx.restore();
    64     }
     62    ctx.restore(); // Ensures the state is reset between test cases.
    6563
    6664    clearTimeout(timeoutID);
    6765    timeoutID = NaN;
    6866
    69     ctx.save();
    70     ctx.save();
     67    ctx.save(); // Ensures the state is reset between test cases.
     68    ctx.save(); // This matches the `restore` call in `performActions`.
     69    saveCount = 1;
     70
    7171    ctx.resetTransform();
    7272    ctx.beginPath();
    7373    ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
    74 
    75     restoreCalled = false;
    7674}
    7775
     
    258256        () => {
    259257            ctx.restore();
    260 
    261             restoreCalled = true;
     258            --saveCount;
    262259        },
    263260        () => {
     
    266263        () => {
    267264            ctx.save();
     265            ++saveCount;
    268266        },
    269267        () => {
     
    409407}
    410408
     409function performSavePreActions() {
     410    cancelActions();
     411    ctx.restore();
     412    ctx.restore();
     413
     414    function saveFillStyle(value) {
     415        ctx.save();
     416        ctx.fillStyle = value;
     417    }
     418
     419    saveFillStyle("#ff0000");
     420    saveFillStyle("#00ff00");
     421    saveFillStyle("#0000ff");
     422}
     423
     424function performSavePostActions() {
     425    ctx.fill();
     426}
     427
    411428function performNaNActions() {
    412429    ctx.globalAlpha = NaN;
     
    484501
    485502    suite.addTestCase({
     503        name: "Canvas.recording2D.ExistingSaves",
     504        description: "Check that existing save calls are sent to the frontend.",
     505        test(resolve, reject) {
     506            let canvas = getCanvas(WI.Canvas.ContextType.Canvas2D);
     507            if (!canvas) {
     508                reject("Missing 2D canvas.");
     509                return;
     510            }
     511
     512            async function logStates(recording) {
     513                async function compare(index, expected) {
     514                    let swizzledState = await recording._swizzleState(recording.initialState.states[index]);
     515                    InspectorTest.expectEqual(swizzledState["fillStyle"], expected, `State ${index} should match expected fillStyle value.`)
     516                }
     517
     518                await compare(0, "#000000");
     519                await compare(1, "#ff0000");
     520                await compare(2, "#00ff00");
     521                await compare(3, "#0000ff");
     522            }
     523
     524            canvas.awaitEvent(WI.Canvas.Event.RecordingStopped)
     525            .then((event) => {
     526                let {recording} = event.data;
     527
     528                InspectorTest.expectEqual(recording.initialState.states.length, 4, "There should be 4 existing states.");
     529
     530                logStates(recording)
     531                .then(resolve, reject);
     532            });
     533
     534            canvas.awaitEvent(WI.Canvas.Event.RecordingStarted)
     535            .then((event) => {
     536                InspectorTest.evaluateInPage(`performSavePostActions()`).catch(reject);
     537            });
     538
     539            InspectorTest.evaluateInPage(`performSavePreActions()`)
     540            .then(() => {
     541                const singleFrame = true;
     542                CanvasAgent.startRecording(canvas.identifier, singleFrame).catch(reject);
     543            }, reject);
     544        },
     545    });
     546
     547    suite.addTestCase({
    486548        name: "Canvas.recording2D.NoActions",
    487549        description: "Check that a canvas is still able to be recorded after stopping a recording with no actions.",
  • trunk/LayoutTests/inspector/canvas/resources/recording-utilities.js

    r237010 r237198  
    55            if (typeof value === "string")
    66                value = sanitizeURL(value);
     7            else if (Array.isArray(value) && value[0] instanceof DOMMatrix)
     8                value[0] = [value[0].a, value[0].b, value[0].c, value[0].d, value[0].e, value[0].f];
    79            InspectorTest.log(indent + key + ": " + JSON.stringify(value));
    810        }
    911    }
    1012
    11     function logRecording(recording) {
     13    async function logRecording(recording) {
    1214        InspectorTest.log("initialState:");
    1315
    1416        InspectorTest.log("  attributes:");
    1517        log(recording.initialState.attributes, "    ");
     18
     19        let currentState = recording.initialState.states.lastValue;
     20        if (currentState) {
     21            InspectorTest.log("  current state:");
     22            let swizzledState = await recording._swizzleState(currentState);
     23            log(swizzledState, "    ");
     24        }
    1625
    1726        InspectorTest.log("  parameters:");
     
    93102                CanvasAgent.stopRecording(canvas.identifier).catch(reject);
    94103            else {
    95                 InspectorTest.evaluateInPage(`cancelActions()`).catch(reject);
    96 
    97                 if (swizzled)
    98                     resolve();
     104                InspectorTest.evaluateInPage(`cancelActions()`)
     105                .then(() => {
     106                    if (swizzled)
     107                        resolve();
     108                }, reject);
    99109            }
    100110        });
     
    125135                swizzled = true;
    126136
    127                 logRecording(recording, type);
    128 
    129                 if (lastFrame) {
    130                     InspectorTest.evaluateInPage(`cancelActions()`)
    131                     .then(resolve, reject);
    132                 }
     137                logRecording(recording, type)
     138                .then(() => {
     139                    if (lastFrame) {
     140                        InspectorTest.evaluateInPage(`cancelActions()`)
     141                        .then(resolve, reject);
     142                    }
     143                }, reject);
    133144            });
    134145        });
  • trunk/LayoutTests/inspector/model/recording-expected.txt

    r224389 r237198  
    3838      "test": "test"
    3939    },
     40    "states": [
     41      {
     42        "test": "test"
     43      }
     44    ],
    4045    "parameters": [
    4146      "test"
     
    6166      "test": "test"
    6267    },
     68    "states": [
     69      {
     70        "test": "test"
     71      }
     72    ],
    6373    "parameters": [
    6474      "test"
     
    93103      "test": "test"
    94104    },
     105    "states": [
     106      {
     107        "test": "test"
     108      }
     109    ],
    95110    "parameters": [
    96111      "test"
     
    125140      "test": "test"
    126141    },
     142    "states": [
     143      {
     144        "test": "test"
     145      }
     146    ],
    127147    "parameters": [
    128148      "test"
  • trunk/LayoutTests/inspector/model/recording.html

    r224389 r237198  
    4949                initialState: {
    5050                    attributes: null,
     51                    states: null,
    5152                    parameters: null,
    5253                    content: null,
     
    6566                        test: "test",
    6667                    },
     68                    states: [
     69                        {
     70                            test: "test",
     71                        },
     72                    ],
    6773                    parameters: ["test"],
    6874                    content: "test",
     
    8793                        test: "test",
    8894                    },
     95                    states: [
     96                        {
     97                            test: "test",
     98                        },
     99                    ],
    89100                    parameters: ["test"],
    90101                    content: "test",
     
    109120                        test: "test",
    110121                    },
     122                    states: [
     123                        {
     124                            test: "test",
     125                        },
     126                    ],
    111127                    parameters: ["test"],
    112128                    content: "test",
     
    138154                        test: "test",
    139155                    },
     156                    states: [
     157                        {
     158                            test: "test",
     159                        },
     160                    ],
    140161                    parameters: ["test"],
    141162                    content: "test",
  • trunk/Source/JavaScriptCore/ChangeLog

    r237197 r237198  
     12018-10-16  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: Canvas: capture previously saved states and add them to the recording payload
     4        https://bugs.webkit.org/show_bug.cgi?id=190473
     5
     6        Reviewed by Joseph Pecoraro.
     7
     8        * inspector/protocol/Recording.json:
     9        Add `states` key to `InitialState` object.
     10
    1112018-10-16  Keith Miller  <keith_miller@apple.com>
    212
  • trunk/Source/JavaScriptCore/inspector/protocol/Recording.json

    r237010 r237198  
    2020            "properties": [
    2121                { "name": "attributes", "type": "object", "optional": true, "description": "Key-value map for each attribute of the state." },
     22                { "name": "states", "type": "array", "items": { "type": "object" }, "optional": true, "description": "Array of saved states of the context." },
    2223                { "name": "parameters", "type": "array", "items": { "type": "any" }, "optional": true, "description": "Array of values that were used to construct the recorded object." },
    2324                { "name": "content", "type": "string", "optional": true, "description": "Current content at the start of the recording." }
  • trunk/Source/WebCore/ChangeLog

    r237192 r237198  
     12018-10-16  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: Canvas: capture previously saved states and add them to the recording payload
     4        https://bugs.webkit.org/show_bug.cgi?id=190473
     5
     6        Reviewed by Joseph Pecoraro.
     7
     8        Updated existing tests: inspector/canvas/recording-2d.html
     9                                inspector/model/recording.html
     10
     11        Instead of sending a single object of the current state of the context, send an array of
     12        objects, one for each restore point.
     13
     14        * html/canvas/CanvasRenderingContext2DBase.h:
     15        * html/canvas/CanvasRenderingContext2DBase.cpp:
     16        (WebCore::CanvasRenderingContext2DBase::stateStack): Added.
     17
     18        * inspector/InspectorCanvas.h:
     19        * inspector/InspectorCanvas.cpp:
     20        (WebCore::InspectorCanvas::stringIndexForKey): Added.
     21        (WebCore::InspectorCanvas::buildInitialState):
     22
    1232018-10-16  Justin Michaud  <justin_michaud@apple.com>
    224
  • trunk/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.cpp

    r234610 r237198  
    20642064}
    20652065
     2066const Vector<CanvasRenderingContext2DBase::State, 1>& CanvasRenderingContext2DBase::stateStack()
     2067{
     2068    realizeSaves();
     2069    return m_stateStack;
     2070}
     2071
    20662072void CanvasRenderingContext2DBase::paintRenderingResultsToCanvas()
    20672073{
  • trunk/Source/WebCore/html/canvas/CanvasRenderingContext2DBase.h

    r227050 r237198  
    275275
    276276    const State& state() const { return m_stateStack.last(); }
     277    const Vector<State, 1>& stateStack();
    277278
    278279protected:
  • trunk/Source/WebCore/inspector/InspectorCanvas.cpp

    r236954 r237198  
    407407}
    408408
     409String InspectorCanvas::stringIndexForKey(const String& key)
     410{
     411    return String::number(indexForData(key));
     412}
     413
    409414static Ref<JSON::ArrayOf<double>> buildArrayForAffineTransform(const AffineTransform& affineTransform)
    410415{
     
    429434Ref<Inspector::Protocol::Recording::InitialState> InspectorCanvas::buildInitialState()
    430435{
    431     auto initialState = Inspector::Protocol::Recording::InitialState::create().release();
    432 
    433     auto attributes = JSON::Object::create();
    434     attributes->setInteger("width"_s, m_context.canvasBase().width());
    435     attributes->setInteger("height"_s, m_context.canvasBase().height());
    436 
    437     auto parameters = JSON::ArrayOf<JSON::Value>::create();
     436    auto initialStatePayload = Inspector::Protocol::Recording::InitialState::create().release();
     437
     438    auto attributesPayload = JSON::Object::create();
     439    attributesPayload->setInteger("width"_s, m_context.canvasBase().width());
     440    attributesPayload->setInteger("height"_s, m_context.canvasBase().height());
     441
     442    auto statesPayload = JSON::ArrayOf<JSON::Object>::create();
     443
     444    auto parametersPayload = JSON::ArrayOf<JSON::Value>::create();
    438445
    439446    if (is<CanvasRenderingContext2D>(m_context)) {
    440447        auto& context2d = downcast<CanvasRenderingContext2D>(m_context);
    441         auto& state = context2d.state();
    442 
    443         attributes->setArray("setTransform"_s, buildArrayForAffineTransform(state.transform));
    444         attributes->setDouble("globalAlpha"_s, context2d.globalAlpha());
    445         attributes->setInteger("globalCompositeOperation"_s, indexForData(context2d.globalCompositeOperation()));
    446         attributes->setDouble("lineWidth"_s, context2d.lineWidth());
    447         attributes->setInteger("lineCap"_s, indexForData(convertEnumerationToString(context2d.lineCap())));
    448         attributes->setInteger("lineJoin"_s, indexForData(convertEnumerationToString(context2d.lineJoin())));
    449         attributes->setDouble("miterLimit"_s, context2d.miterLimit());
    450         attributes->setDouble("shadowOffsetX"_s, context2d.shadowOffsetX());
    451         attributes->setDouble("shadowOffsetY"_s, context2d.shadowOffsetY());
    452         attributes->setDouble("shadowBlur"_s, context2d.shadowBlur());
    453         attributes->setInteger("shadowColor"_s, indexForData(context2d.shadowColor()));
    454 
    455         // The parameter to `setLineDash` is itself an array, so we need to wrap the parameters
    456         // list in an array to allow spreading.
    457         auto setLineDash = JSON::ArrayOf<JSON::Value>::create();
    458         setLineDash->addItem(buildArrayForVector(state.lineDash));
    459         attributes->setArray("setLineDash"_s, WTFMove(setLineDash));
    460 
    461         attributes->setDouble("lineDashOffset"_s, context2d.lineDashOffset());
    462         attributes->setInteger("font"_s, indexForData(context2d.font()));
    463         attributes->setInteger("textAlign"_s, indexForData(convertEnumerationToString(context2d.textAlign())));
    464         attributes->setInteger("textBaseline"_s, indexForData(convertEnumerationToString(context2d.textBaseline())));
    465         attributes->setInteger("direction"_s, indexForData(convertEnumerationToString(context2d.direction())));
    466 
    467         int strokeStyleIndex;
    468         if (auto canvasGradient = state.strokeStyle.canvasGradient())
    469             strokeStyleIndex = indexForData(canvasGradient.get());
    470         else if (auto canvasPattern = state.strokeStyle.canvasPattern())
    471             strokeStyleIndex = indexForData(canvasPattern.get());
    472         else
    473             strokeStyleIndex = indexForData(state.strokeStyle.color());
    474         attributes->setInteger("strokeStyle"_s, strokeStyleIndex);
    475 
    476         int fillStyleIndex;
    477         if (auto canvasGradient = state.fillStyle.canvasGradient())
    478             fillStyleIndex = indexForData(canvasGradient.get());
    479         else if (auto canvasPattern = state.fillStyle.canvasPattern())
    480             fillStyleIndex = indexForData(canvasPattern.get());
    481         else
    482             fillStyleIndex = indexForData(state.fillStyle.color());
    483         attributes->setInteger("fillStyle"_s, fillStyleIndex);
    484 
    485         attributes->setBoolean("imageSmoothingEnabled"_s, context2d.imageSmoothingEnabled());
    486         attributes->setInteger("imageSmoothingQuality"_s, indexForData(convertEnumerationToString(context2d.imageSmoothingQuality())));
    487 
    488         auto setPath = JSON::ArrayOf<JSON::Value>::create();
    489         setPath->addItem(indexForData(buildStringFromPath(context2d.getPath()->path())));
    490         attributes->setArray("setPath"_s, WTFMove(setPath));
     448        for (auto& state : context2d.stateStack()) {
     449            RefPtr<JSON::Object> statePayload = JSON::Object::create();
     450
     451            statePayload->setArray(stringIndexForKey("setTransform"_s), buildArrayForAffineTransform(state.transform));
     452            statePayload->setDouble(stringIndexForKey("globalAlpha"_s), context2d.globalAlpha());
     453            statePayload->setInteger(stringIndexForKey("globalCompositeOperation"_s), indexForData(context2d.globalCompositeOperation()));
     454            statePayload->setDouble(stringIndexForKey("lineWidth"_s), context2d.lineWidth());
     455            statePayload->setInteger(stringIndexForKey("lineCap"_s), indexForData(convertEnumerationToString(context2d.lineCap())));
     456            statePayload->setInteger(stringIndexForKey("lineJoin"_s), indexForData(convertEnumerationToString(context2d.lineJoin())));
     457            statePayload->setDouble(stringIndexForKey("miterLimit"_s), context2d.miterLimit());
     458            statePayload->setDouble(stringIndexForKey("shadowOffsetX"_s), context2d.shadowOffsetX());
     459            statePayload->setDouble(stringIndexForKey("shadowOffsetY"_s), context2d.shadowOffsetY());
     460            statePayload->setDouble(stringIndexForKey("shadowBlur"_s), context2d.shadowBlur());
     461            statePayload->setInteger(stringIndexForKey("shadowColor"_s), indexForData(context2d.shadowColor()));
     462
     463            // The parameter to `setLineDash` is itself an array, so we need to wrap the parameters
     464            // list in an array to allow spreading.
     465            auto setLineDash = JSON::ArrayOf<JSON::Value>::create();
     466            setLineDash->addItem(buildArrayForVector(state.lineDash));
     467            statePayload->setArray(stringIndexForKey("setLineDash"_s), WTFMove(setLineDash));
     468
     469            statePayload->setDouble(stringIndexForKey("lineDashOffset"_s), context2d.lineDashOffset());
     470            statePayload->setInteger(stringIndexForKey("font"_s), indexForData(context2d.font()));
     471            statePayload->setInteger(stringIndexForKey("textAlign"_s), indexForData(convertEnumerationToString(context2d.textAlign())));
     472            statePayload->setInteger(stringIndexForKey("textBaseline"_s), indexForData(convertEnumerationToString(context2d.textBaseline())));
     473            statePayload->setInteger(stringIndexForKey("direction"_s), indexForData(convertEnumerationToString(context2d.direction())));
     474
     475            int strokeStyleIndex;
     476            if (auto canvasGradient = state.strokeStyle.canvasGradient())
     477                strokeStyleIndex = indexForData(canvasGradient.get());
     478            else if (auto canvasPattern = state.strokeStyle.canvasPattern())
     479                strokeStyleIndex = indexForData(canvasPattern.get());
     480            else
     481                strokeStyleIndex = indexForData(state.strokeStyle.color());
     482            statePayload->setInteger(stringIndexForKey("strokeStyle"_s), strokeStyleIndex);
     483
     484            int fillStyleIndex;
     485            if (auto canvasGradient = state.fillStyle.canvasGradient())
     486                fillStyleIndex = indexForData(canvasGradient.get());
     487            else if (auto canvasPattern = state.fillStyle.canvasPattern())
     488                fillStyleIndex = indexForData(canvasPattern.get());
     489            else
     490                fillStyleIndex = indexForData(state.fillStyle.color());
     491            statePayload->setInteger(stringIndexForKey("fillStyle"_s), fillStyleIndex);
     492
     493            statePayload->setBoolean(stringIndexForKey("imageSmoothingEnabled"_s), context2d.imageSmoothingEnabled());
     494            statePayload->setInteger(stringIndexForKey("imageSmoothingQuality"_s), indexForData(convertEnumerationToString(context2d.imageSmoothingQuality())));
     495
     496            auto setPath = JSON::ArrayOf<JSON::Value>::create();
     497            setPath->addItem(indexForData(buildStringFromPath(context2d.getPath()->path())));
     498            statePayload->setArray(stringIndexForKey("setPath"_s), WTFMove(setPath));
     499
     500            statesPayload->addItem(WTFMove(statePayload));
     501        }
    491502    }
    492503#if ENABLE(WEBGL)
    493504    else if (is<WebGLRenderingContextBase>(m_context)) {
    494505        WebGLRenderingContextBase& contextWebGLBase = downcast<WebGLRenderingContextBase>(m_context);
    495         if (std::optional<WebGLContextAttributes> attributes = contextWebGLBase.getContextAttributes()) {
    496             RefPtr<JSON::Object> contextAttributes = JSON::Object::create();
    497             contextAttributes->setBoolean("alpha"_s, attributes->alpha);
    498             contextAttributes->setBoolean("depth"_s, attributes->depth);
    499             contextAttributes->setBoolean("stencil"_s, attributes->stencil);
    500             contextAttributes->setBoolean("antialias"_s, attributes->antialias);
    501             contextAttributes->setBoolean("premultipliedAlpha"_s, attributes->premultipliedAlpha);
    502             contextAttributes->setBoolean("preserveDrawingBuffer"_s, attributes->preserveDrawingBuffer);
    503             contextAttributes->setBoolean("failIfMajorPerformanceCaveat"_s, attributes->failIfMajorPerformanceCaveat);
    504             parameters->addItem(WTFMove(contextAttributes));
     506        if (std::optional<WebGLContextAttributes> webGLContextAttributes = contextWebGLBase.getContextAttributes()) {
     507            RefPtr<JSON::Object> webGLContextAttributesPayload = JSON::Object::create();
     508            webGLContextAttributesPayload->setBoolean("alpha"_s, webGLContextAttributes->alpha);
     509            webGLContextAttributesPayload->setBoolean("depth"_s, webGLContextAttributes->depth);
     510            webGLContextAttributesPayload->setBoolean("stencil"_s, webGLContextAttributes->stencil);
     511            webGLContextAttributesPayload->setBoolean("antialias"_s, webGLContextAttributes->antialias);
     512            webGLContextAttributesPayload->setBoolean("premultipliedAlpha"_s, webGLContextAttributes->premultipliedAlpha);
     513            webGLContextAttributesPayload->setBoolean("preserveDrawingBuffer"_s, webGLContextAttributes->preserveDrawingBuffer);
     514            webGLContextAttributesPayload->setBoolean("failIfMajorPerformanceCaveat"_s, webGLContextAttributes->failIfMajorPerformanceCaveat);
     515            parametersPayload->addItem(WTFMove(webGLContextAttributesPayload));
    505516        }
    506517    }
    507518#endif
    508519
    509     initialState->setAttributes(WTFMove(attributes));
    510 
    511     if (parameters->length())
    512         initialState->setParameters(WTFMove(parameters));
    513 
    514     initialState->setContent(getCanvasContentAsDataURL());
    515 
    516     return initialState;
     520    initialStatePayload->setAttributes(WTFMove(attributesPayload));
     521
     522    if (statesPayload->length())
     523        initialStatePayload->setStates(WTFMove(statesPayload));
     524
     525    if (parametersPayload->length())
     526        initialStatePayload->setParameters(WTFMove(parametersPayload));
     527
     528    initialStatePayload->setContent(getCanvasContentAsDataURL());
     529
     530    return initialStatePayload;
    517531}
    518532
  • trunk/Source/WebCore/inspector/InspectorCanvas.h

    r237010 r237198  
    9797
    9898    int indexForData(DuplicateDataVariant);
     99    String stringIndexForKey(const String&);
    99100    Ref<Inspector::Protocol::Recording::InitialState> buildInitialState();
    100101    Ref<JSON::ArrayOf<JSON::Value>> buildAction(const String&, Vector<RecordCanvasActionVariant>&& = { });
  • trunk/Source/WebInspectorUI/ChangeLog

    r237196 r237198  
     12018-10-16  Devin Rousso  <drousso@apple.com>
     2
     3        Web Inspector: Canvas: capture previously saved states and add them to the recording payload
     4        https://bugs.webkit.org/show_bug.cgi?id=190473
     5
     6        Reviewed by Joseph Pecoraro.
     7
     8        Instead of sending a single object of the current state of the context, send an array of
     9        objects, one for each restore point. When replaying, recreate each restore point before
     10        applying the selected action(s).
     11
     12        * UserInterface/Models/Recording.js:
     13        (WI.Recording):
     14        (WI.Recording.fromPayload):
     15        (WI.Recording.prototype.toJSON):
     16        (WI.Recording.prototype.async._process):
     17        (WI.Recording.prototype.async._swizzleState): Added.
     18        * UserInterface/Models/RecordingAction.js:
     19        (WI.RecordingAction):
     20        (WI.RecordingAction.deriveCurrentState): Added.
     21        (WI.RecordingAction.prototype.get states): Added.
     22        (WI.RecordingAction.prototype.process):
     23        (WI.RecordingAction.prototype.get state): Deleted.
     24        Drive-by: when `process`ing, also check to see if any values in the current state changed
     25        outside of those expected in `_stateModifiers` (e.g. `restore` may modify some state values).
     26
     27        * UserInterface/Views/RecordingContentView.js:
     28        (WI.RecordingContentView.prototype._generateContentCanvas2D):
     29
     30        * UserInterface/Views/RecordingStateDetailsSidebarPanel.js:
     31        (WI.RecordingStateDetailsSidebarPanel.prototype._generateDetailsCanvas2D):
     32        Default to showing the most recent (current) state.
     33
     34        * UserInterface/Views/CanvasTabContentView.js:
     35        (WI.CanvasTabContentView.prototype.initialLayout): Added.
     36        (WI.CanvasTabContentView.prototype._addCanvas):
     37        (WI.CanvasTabContentView.prototype._removeCanvas):
     38        (WI.CanvasTabContentView.prototype._addRecording): Added.
     39        (WI.CanvasTabContentView.prototype._recordingImportedOrStopped):
     40        (WI.CanvasTabContentView.prototype._recordingAdded): Deleted.
     41        * UserInterface/Controllers/CanvasManager.js:
     42        (WI.CanvasManager):
     43        (WI.CanvasManager.prototype.get importedRecordings): Added.
     44        (WI.CanvasManager.prototype.importRecording):
     45        Drive-by: store imported recordings on `WI.CanvasManager` so that if the Canvas tab is
     46        closed we can still show the list of imported recordings.
     47
    1482018-10-16  Devin Rousso  <drousso@apple.com>
    249
  • trunk/Source/WebInspectorUI/UserInterface/Controllers/CanvasManager.js

    r237090 r237198  
    3434        this._canvasIdentifierMap = new Map;
    3535        this._shaderProgramIdentifierMap = new Map;
     36        this._importedRecordings = new Set;
    3637
    3738        if (window.CanvasAgent)
     
    4041
    4142    // Public
     43
     44    get importedRecordings() { return this._importedRecordings; }
    4245
    4346    get canvases()
     
    7679            recording.createDisplayName(filename);
    7780
     81            this._importedRecordings.add(recording);
     82
    7883            this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingImported, {recording});
    7984        });
  • trunk/Source/WebInspectorUI/UserInterface/Models/Recording.js

    r237010 r237198  
    4343
    4444        this._processContext = null;
     45        this._processStates = [];
    4546        this._processing = false;
    4647    }
     
    7475        if (typeof payload.initialState.attributes !== "object" || payload.initialState.attributes === null)
    7576            payload.initialState.attributes = {};
     77        if (!Array.isArray(payload.initialState.states) || payload.initialState.states.some((item) => typeof item !== "object" || item === null)) {
     78            payload.initialState.states = [];
     79
     80            // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
     81            if (!isEmptyObject(payload.initialState.attributes)) {
     82                let {width, height, ...state} = payload.initialState.attributes;
     83                if (!isEmptyObject(state))
     84                    payload.initialState.states.push(state);
     85            }
     86        }
    7687        if (!Array.isArray(payload.initialState.parameters))
    7788            payload.initialState.parameters = [];
     
    338349        if (!isEmptyObject(this._initialState.attributes))
    339350            initialState.attributes = this._initialState.attributes;
     351        if (this._initialState.states.length)
     352            initialState.states = this._initialState.states;
    340353        if (this._initialState.parameters.length)
    341354            initialState.parameters = this._initialState.parameters;
     
    363376                this._processContext.drawImage(initialContent, 0, 0);
    364377
    365                 for (let [key, value] of Object.entries(this._initialState.attributes)) {
    366                     switch (key) {
    367                     case "setTransform":
    368                         value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
    369                         break;
    370 
    371                     case "fillStyle":
    372                     case "strokeStyle":
    373                             let [gradient, pattern, string] = await Promise.all([
    374                                 this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
    375                                 this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
    376                                 this.swizzle(value, WI.Recording.Swizzle.String),
    377                             ]);
    378                             if (gradient && !pattern)
    379                                 value = gradient;
    380                             else if (pattern && !gradient)
    381                                 value = pattern;
     378                for (let state of this._initialState.states) {
     379                    let swizzledState = await this._swizzleState(state);
     380                    for (let [key, value] of Object.entries(swizzledState)) {
     381                        try {
     382                            if (WI.RecordingAction.isFunctionForType(this._type, key))
     383                                this._processContext[key](...value);
    382384                            else
    383                                 value = string;
    384                         break;
    385 
    386                     case "direction":
    387                     case "font":
    388                     case "globalCompositeOperation":
    389                     case "imageSmoothingEnabled":
    390                     case "imageSmoothingQuality":
    391                     case "lineCap":
    392                     case "lineJoin":
    393                     case "shadowColor":
    394                     case "textAlign":
    395                     case "textBaseline":
    396                         value = await this.swizzle(value, WI.Recording.Swizzle.String);
    397                         break;
    398 
    399                     case "setPath":
    400                         value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
    401                         break;
     385                                this._processContext[key] = value;
     386                        } catch { }
    402387                    }
    403388
    404                     if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
    405                         continue;
    406 
    407                     try {
    408                         if (WI.RecordingAction.isFunctionForType(this._type, key))
    409                             this._processContext[key](...value);
    410                         else
    411                             this._processContext[key] = value;
    412                     } catch { }
     389                    // The last state represents the current state, which should not be saved.
     390                    if (state !== this._initialState.states.lastValue) {
     391                        this._processContext.save();
     392                        this._processStates.push(WI.RecordingAction.deriveCurrentState(this._type, this._processContext));
     393                    }
    413394                }
    414395            }
     
    418399        // Since it is not associated with a WI.RecordingFrame, it has to manually process().
    419400        if (!this._actions[0].ready) {
    420             this._actions[0].process(this, this._processContext);
     401            this._actions[0].process(this, this._processContext, this._processStates);
    421402            this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0});
    422403        }
     
    426407
    427408        let cumulativeActionIndex = 0;
     409        let lastAction = this._actions[cumulativeActionIndex];
    428410        for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) {
    429411            let frame = this._frames[frameIndex];
     
    431413            if (frame.actions.lastValue.ready) {
    432414                cumulativeActionIndex += frame.actions.length;
     415                lastAction = frame.actions.lastValue;
    433416                continue;
    434417            }
     
    438421
    439422                let action = frame.actions[actionIndex];
    440                 if (action.ready)
     423                if (action.ready) {
     424                    lastAction = action;
    441425                    continue;
     426                }
    442427
    443428                await action.swizzle(this);
    444429
    445                 action.process(this, this._processContext);
     430                action.process(this, this._processContext, this._processStates, {lastAction});
    446431
    447432                if (action.isVisual)
     
    458443                    startTime = Date.now();
    459444                }
     445
     446                lastAction = action;
    460447
    461448                if (!this._processing)
     
    469456        this._processContext = null;
    470457        this._processing = false;
     458    }
     459
     460    async _swizzleState(state)
     461    {
     462        let swizzledState = {};
     463
     464        for (let [key, value] of Object.entries(state)) {
     465            // COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
     466            let keyIndex = parseInt(key);
     467            if (!isNaN(keyIndex))
     468                key = await this.swizzle(keyIndex, WI.Recording.Swizzle.String);
     469
     470            switch (key) {
     471            case "setTransform":
     472                value = [await this.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
     473                break;
     474
     475            case "fillStyle":
     476            case "strokeStyle":
     477                    let [gradient, pattern, string] = await Promise.all([
     478                        this.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
     479                        this.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
     480                        this.swizzle(value, WI.Recording.Swizzle.String),
     481                    ]);
     482                    if (gradient && !pattern)
     483                        value = gradient;
     484                    else if (pattern && !gradient)
     485                        value = pattern;
     486                    else
     487                        value = string;
     488                break;
     489
     490            case "direction":
     491            case "font":
     492            case "globalCompositeOperation":
     493            case "imageSmoothingQuality":
     494            case "lineCap":
     495            case "lineJoin":
     496            case "shadowColor":
     497            case "textAlign":
     498            case "textBaseline":
     499                value = await this.swizzle(value, WI.Recording.Swizzle.String);
     500                break;
     501
     502            case "globalAlpha":
     503            case "lineWidth":
     504            case "miterLimit":
     505            case "shadowOffsetX":
     506            case "shadowOffsetY":
     507            case "shadowBlur":
     508            case "lineDashOffset":
     509                value = await this.swizzle(value, WI.Recording.Swizzle.Number);
     510                break;
     511
     512            case "setPath":
     513                value = [await this.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
     514                break;
     515            }
     516
     517            if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
     518                continue;
     519
     520            swizzledState[key] = value;
     521        }
     522
     523        return swizzledState;
    471524    }
    472525};
  • trunk/Source/WebInspectorUI/UserInterface/Models/RecordingAction.js

    r236715 r237198  
    4747        this._hasVisibleEffect = undefined;
    4848
    49         this._state = null;
     49        this._states = [];
    5050        this._stateModifiers = new Set;
    5151
     
    131131    }
    132132
    133     static _prototypeForType(type)
    134     {
    135         if (type === WI.Recording.Type.Canvas2D)
    136             return CanvasRenderingContext2D.prototype;
    137         if (type === WI.Recording.Type.CanvasBitmapRenderer)
    138             return ImageBitmapRenderingContext.prototype;
    139         if (type === WI.Recording.Type.CanvasWebGL)
    140             return WebGLRenderingContext.prototype;
    141         return null;
    142     }
    143 
    144     // Public
    145 
    146     get name() { return this._name; }
    147     get parameters() { return this._parameters; }
    148     get swizzleTypes() { return this._payloadSwizzleTypes; }
    149     get trace() { return this._trace; }
    150     get snapshot() { return this._snapshot; }
    151     get valid() { return this._valid; }
    152     get isFunction() { return this._isFunction; }
    153     get isGetter() { return this._isGetter; }
    154     get isVisual() { return this._isVisual; }
    155     get hasVisibleEffect() { return this._hasVisibleEffect; }
    156     get state() { return this._state; }
    157     get stateModifiers() { return this._stateModifiers; }
    158 
    159     get ready()
    160     {
    161         return this._swizzled && this._processed;
    162     }
    163 
    164     process(recording, context)
    165     {
    166         console.assert(this._swizzled, "You must swizzle() before you can process().");
    167         console.assert(!this._processed, "You should only process() once.");
    168 
    169         this._processed = true;
    170 
    171         if (recording.type === WI.Recording.Type.CanvasWebGL) {
    172             // We add each RecordingAction to the list of visualActionIndexes after it is processed.
    173             if (this._valid && this._isVisual) {
    174                 let contentBefore = recording.visualActionIndexes.length ? recording.actions[recording.visualActionIndexes.lastValue].snapshot : recording.initialState.content;
    175                 this._hasVisibleEffect = this._snapshot !== contentBefore;
    176             }
    177             return;
    178         }
    179 
    180         function getContent() {
    181             if (context instanceof CanvasRenderingContext2D)
    182                 return context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
    183 
    184             if (context instanceof WebGLRenderingContext || context instanceof WebGL2RenderingContext) {
    185                 let pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4);
    186                 context.readPixels(0, 0, context.canvas.width, context.canvas.height, context.RGBA, context.UNSIGNED_BYTE, pixels);
    187                 return pixels;
    188             }
    189 
    190             if (context.canvas instanceof HTMLCanvasElement)
    191                 return [context.canvas.toDataURL()];
    192 
    193             console.assert("Unknown context type", context);
    194             return [];
    195         }
    196 
    197         let contentBefore = null;
    198         let shouldCheckHasVisualEffect = this._valid && this._isVisual;
    199         if (shouldCheckHasVisualEffect)
    200             contentBefore = getContent();
    201 
    202         this.apply(context);
    203 
    204         if (shouldCheckHasVisualEffect)
    205             this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
    206 
    207         if (recording.type === WI.Recording.Type.Canvas2D) {
     133    static deriveCurrentState(type, context)
     134    {
     135        if (type === WI.Recording.Type.Canvas2D) {
    208136            let matrix = context.getTransform();
    209137
    210             this._state = {
     138            let state = {
    211139                currentX: context.currentX,
    212140                currentY: context.currentY,
     
    238166
    239167            if (WI.ImageUtilities.supportsCanvasPathDebugging())
    240                 this._state.setPath = [context.getPath()];
    241         }
    242     }
    243 
    244     async swizzle(recording)
     168                state.setPath = [context.getPath()];
     169
     170            return state;
     171        }
     172
     173        return null;
     174    }
     175
     176    static _prototypeForType(type)
     177    {
     178        if (type === WI.Recording.Type.Canvas2D)
     179            return CanvasRenderingContext2D.prototype;
     180        if (type === WI.Recording.Type.CanvasBitmapRenderer)
     181            return ImageBitmapRenderingContext.prototype;
     182        if (type === WI.Recording.Type.CanvasWebGL)
     183            return WebGLRenderingContext.prototype;
     184        return null;
     185    }
     186
     187    // Public
     188
     189    get name() { return this._name; }
     190    get parameters() { return this._parameters; }
     191    get swizzleTypes() { return this._payloadSwizzleTypes; }
     192    get trace() { return this._trace; }
     193    get snapshot() { return this._snapshot; }
     194    get valid() { return this._valid; }
     195    get isFunction() { return this._isFunction; }
     196    get isGetter() { return this._isGetter; }
     197    get isVisual() { return this._isVisual; }
     198    get hasVisibleEffect() { return this._hasVisibleEffect; }
     199    get states() { return this._states; }
     200    get stateModifiers() { return this._stateModifiers; }
     201
     202    get ready()
     203    {
     204        return this._swizzled && this._processed;
     205    }
     206
     207    process(recording, context, states, {lastAction} = {})
     208    {
     209        console.assert(this._swizzled, "You must swizzle() before you can process().");
     210        console.assert(!this._processed, "You should only process() once.");
     211
     212        this._processed = true;
     213
     214        if (recording.type === WI.Recording.Type.CanvasWebGL) {
     215            // We add each RecordingAction to the list of visualActionIndexes after it is processed.
     216            if (this._valid && this._isVisual) {
     217                let contentBefore = recording.visualActionIndexes.length ? recording.actions[recording.visualActionIndexes.lastValue].snapshot : recording.initialState.content;
     218                this._hasVisibleEffect = this._snapshot !== contentBefore;
     219            }
     220            return;
     221        }
     222
     223        function getContent() {
     224            if (context instanceof CanvasRenderingContext2D)
     225                return context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
     226
     227            if (context instanceof WebGLRenderingContext || context instanceof WebGL2RenderingContext) {
     228                let pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4);
     229                context.readPixels(0, 0, context.canvas.width, context.canvas.height, context.RGBA, context.UNSIGNED_BYTE, pixels);
     230                return pixels;
     231            }
     232
     233            if (context.canvas instanceof HTMLCanvasElement)
     234                return [context.canvas.toDataURL()];
     235
     236            console.assert("Unknown context type", context);
     237            return [];
     238        }
     239
     240        let contentBefore = null;
     241        let shouldCheckHasVisualEffect = this._valid && this._isVisual;
     242        if (shouldCheckHasVisualEffect)
     243            contentBefore = getContent();
     244
     245        this.apply(context);
     246
     247        if (shouldCheckHasVisualEffect)
     248            this._hasVisibleEffect = !Array.shallowEqual(contentBefore, getContent());
     249
     250        if (recording.type === WI.Recording.Type.Canvas2D) {
     251            let currentState = WI.RecordingAction.deriveCurrentState(recording.type, context);
     252            console.assert(currentState);
     253
     254            if (this.name === "save")
     255                states.push(currentState);
     256            else if (this.name === "restore")
     257                states.pop();
     258
     259            this._states = states.slice();
     260            this._states.push(currentState);
     261
     262            if (lastAction) {
     263                let lastState = lastAction.states.lastValue;
     264                for (let key in currentState) {
     265                    if (!(key in lastState) || (currentState[key] !== lastState[key] && !Object.shallowEqual(currentState[key], lastState[key])))
     266                        this._stateModifiers.add(key);
     267                }
     268            }
     269        }
     270    }
     271
     272    async swizzle(recording, lastAction)
    245273    {
    246274        console.assert(!this._swizzled, "You should only swizzle() once.");
  • trunk/Source/WebInspectorUI/UserInterface/Views/CanvasTabContentView.js

    r237090 r237198  
    119119    // Protected
    120120
     121    initialLayout()
     122    {
     123        super.initialLayout();
     124
     125        const options = {
     126            suppressShowRecording: true,
     127        };
     128
     129        for (let recording of WI.canvasManager.importedRecordings)
     130            this._addRecording(recording, options);
     131    }
     132
    121133    attached()
    122134    {
     
    157169
    158170        for (let recording of canvas.recordingCollection)
    159             this._recordingAdded(recording, {suppressShowRecording: true});
     171            this._addRecording(recording, {suppressShowRecording: true});
    160172    }
    161173
     
    172184
    173185        for (let recording of canvas.recordingCollection)
    174             this._recordingAdded(recording, options);
     186            this._addRecording(recording, options);
    175187
    176188        let currentContentView = this.contentBrowser.currentContentView;
     
    183195    }
    184196
    185     _handleCanvasAdded(event)
    186     {
    187         this._addCanvas(event.data.canvas);
    188     }
    189 
    190     _handleCanvasRemoved(event)
    191     {
    192         this._removeCanvas(event.data.canvas);
    193     }
    194 
    195     _canvasTreeOutlineSelectionDidChange(event)
    196     {
    197         let selectedElement = event.data.selectedElement;
    198         if (!selectedElement)
    199             return;
    200 
    201         let representedObject = selectedElement.representedObject;
    202         if (!this.canShowRepresentedObject(representedObject)) {
    203             console.assert(false, "Unexpected representedObject.", representedObject);
    204             return;
    205         }
    206 
    207         this.showRepresentedObject(representedObject);
    208     }
    209 
    210     _recordingImportedOrStopped(event)
    211     {
    212         let recording = event.data.recording;
    213         if (!recording)
    214             return;
    215 
    216         this._recordingAdded(recording, {
    217             suppressShowRecording: event.data.fromConsole || this.contentBrowser.currentRepresentedObjects.some((representedObject) => representedObject instanceof WI.Recording),
    218         });
    219     }
    220 
    221     _recordingAdded(recording, options = {})
     197    _addRecording(recording, options = {})
    222198    {
    223199        if (!recording.source) {
     
    232208    }
    233209
     210    _handleCanvasAdded(event)
     211    {
     212        this._addCanvas(event.data.canvas);
     213    }
     214
     215    _handleCanvasRemoved(event)
     216    {
     217        this._removeCanvas(event.data.canvas);
     218    }
     219
     220    _canvasTreeOutlineSelectionDidChange(event)
     221    {
     222        let selectedElement = event.data.selectedElement;
     223        if (!selectedElement)
     224            return;
     225
     226        let representedObject = selectedElement.representedObject;
     227        if (!this.canShowRepresentedObject(representedObject)) {
     228            console.assert(false, "Unexpected representedObject.", representedObject);
     229            return;
     230        }
     231
     232        this.showRepresentedObject(representedObject);
     233    }
     234
     235    _recordingImportedOrStopped(event)
     236    {
     237        let recording = event.data.recording;
     238        if (!recording)
     239            return;
     240
     241        this._addRecording(recording, {
     242            suppressShowRecording: event.data.fromConsole || this.contentBrowser.currentRepresentedObjects.some((representedObject) => representedObject instanceof WI.Recording),
     243        });
     244    }
     245
    234246    _handleSpace(event)
    235247    {
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingContentView.js

    r236539 r237198  
    247247            }
    248248
    249             for (let name in snapshot.state) {
    250                 if (!(name in snapshot.context))
    251                     continue;
    252 
    253                 // Skip internal state used for path debugging.
    254                 if (name === "currentX" || name === "currentY")
    255                     continue;
    256 
    257                 try {
    258                     if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
    259                         snapshot.context[name](...snapshot.state[name]);
    260                     else
    261                         snapshot.context[name] = snapshot.state[name];
    262                 } catch {
    263                     delete snapshot.state[name];
     249            for (let state of snapshot.states) {
     250                for (let name in state) {
     251                    if (!(name in snapshot.context))
     252                        continue;
     253
     254                    // Skip internal state used for path debugging.
     255                    if (name === "currentX" || name === "currentY")
     256                        continue;
     257
     258                    try {
     259                        if (WI.RecordingAction.isFunctionForType(this.representedObject.type, name))
     260                            snapshot.context[name](...state[name]);
     261                        else
     262                            snapshot.context[name] = state[name];
     263                    } catch {
     264                        delete state[name];
     265                    }
    264266                }
     267
     268                ++saveCount;
     269                snapshot.context.save();
    265270            }
    266271
     
    292297                    if (!saveCount) // Only attempt to restore if save has been called.
    293298                        continue;
    294                     --saveCount;
    295299                }
    296300
     
    354358            if (lastSnapshotIndex < 0) {
    355359                snapshot.content = this._initialContent;
    356                 snapshot.state = actions[0].state;
     360                snapshot.states = actions[0].states;
    357361            } else {
    358362                snapshot.content = this._snapshots[lastSnapshotIndex].content;
    359                 snapshot.state = this._snapshots[lastSnapshotIndex].state;
     363                snapshot.states = this._snapshots[lastSnapshotIndex].states;
    360364                startIndex = this._snapshots[lastSnapshotIndex].index;
    361365            }
     
    363367            applyActions(startIndex, snapshot.index - 1);
    364368            if (snapshot.index > 0)
    365                 snapshot.state = actions[snapshot.index - 1].state;
     369                snapshot.states = actions[snapshot.index - 1].states;
    366370
    367371            snapshot.content = new Image;
  • trunk/Source/WebInspectorUI/UserInterface/Views/RecordingStateDetailsSidebarPanel.js

    r237196 r237198  
    9797        this._dataGrid.removeChildren();
    9898
    99         console.assert(action.state);
    100         if (!action.state)
     99        let currentState = action.states.lastValue;
     100        console.assert(currentState);
     101        if (!currentState)
    101102            return;
    102103
     
    114115        }
    115116
    116         for (let name in action.state) {
     117        for (let name in currentState) {
    117118            // Skip internal state used for path debugging.
    118119            if (name === "setPath")
    119120                continue;
    120121
    121             let value = action.state[name];
     122            let value = currentState[name];
    122123            if (typeof value === "object") {
    123124                let isGradient = value instanceof CanvasGradient;
Note: See TracChangeset for help on using the changeset viewer.