| /* |
| * Copyright (C) 2017 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| WI.RecordingAction = class RecordingAction extends WI.Object |
| { |
| constructor(name, parameters, swizzleTypes, trace, snapshot) |
| { |
| super(); |
| |
| this._payloadName = name; |
| this._payloadParameters = parameters; |
| this._payloadSwizzleTypes = swizzleTypes; |
| this._payloadTrace = trace; |
| this._payloadSnapshot = snapshot || -1; |
| |
| this._name = ""; |
| this._parameters = []; |
| this._trace = []; |
| this._snapshot = ""; |
| |
| this._valid = true; |
| this._isFunction = false; |
| this._isGetter = false; |
| this._isVisual = false; |
| |
| this._contextReplacer = null; |
| |
| this._states = []; |
| this._stateModifiers = new Set; |
| |
| this._warning = null; |
| this._swizzled = false; |
| this._processed = false; |
| } |
| |
| // Static |
| |
| // Payload format: (name, parameters, swizzleTypes, [trace, [snapshot]]) |
| static fromPayload(payload) |
| { |
| if (!Array.isArray(payload)) |
| payload = []; |
| |
| if (typeof payload[0] !== "number") { |
| if (payload.length > 0) |
| WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("name"))); |
| |
| payload[0] = -1; |
| } |
| |
| if (!Array.isArray(payload[1])) { |
| if (payload.length > 1) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("parameters"))); |
| |
| payload[1] = []; |
| } |
| |
| if (!Array.isArray(payload[2])) { |
| if (payload.length > 2) |
| WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("swizzleTypes"))); |
| |
| payload[2] = []; |
| } |
| |
| if (typeof payload[3] !== "number" || isNaN(payload[3]) || (!payload[3] && payload[3] !== 0)) { |
| // COMPATIBILITY (iOS 12.1): "trace" was sent as an array of call frames instead of a single call stack |
| if (!Array.isArray(payload[3])) { |
| if (payload.length > 3) |
| WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("trace"))); |
| |
| payload[3] = []; |
| } |
| } |
| |
| if (typeof payload[4] !== "number" || isNaN(payload[4])) { |
| if (payload.length > 4) |
| WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("snapshot"))); |
| |
| payload[4] = -1; |
| } |
| |
| return new WI.RecordingAction(...payload); |
| } |
| |
| static isFunctionForType(type, name) |
| { |
| let prototype = WI.RecordingAction._prototypeForType(type); |
| if (!prototype) |
| return false; |
| let propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, name); |
| if (!propertyDescriptor) |
| return false; |
| return typeof propertyDescriptor.value === "function"; |
| } |
| |
| static constantNameForParameter(type, name, value, index, count) |
| { |
| let indexesForType = WI.RecordingAction._constantIndexes[type]; |
| if (!indexesForType) |
| return null; |
| |
| let indexesForAction = indexesForType[name]; |
| if (!indexesForAction) |
| return null; |
| |
| if (Array.isArray(indexesForAction) && !indexesForAction.includes(index)) |
| return null; |
| |
| if (typeof indexesForAction === "object") { |
| let indexesForActionVariant = indexesForAction[count]; |
| if (!indexesForActionVariant) |
| return null; |
| |
| if (Array.isArray(indexesForActionVariant) && !indexesForActionVariant.includes(index)) |
| return null; |
| } |
| |
| if (value === 0 && (type === WI.Recording.Type.CanvasWebGL || type === WI.Recording.Type.CanvasWebGL2)) { |
| if (name === "blendFunc" || name === "blendFuncSeparate") |
| return "ZERO"; |
| if (index === 0) { |
| if (name === "drawArrays" || name === "drawElements") |
| return "POINTS"; |
| if (name === "pixelStorei") |
| return "NONE"; |
| } |
| } |
| |
| let prototype = WI.RecordingAction._prototypeForType(type); |
| if (prototype) { |
| for (let key in prototype) { |
| let descriptor = Object.getOwnPropertyDescriptor(prototype, key); |
| if (descriptor && descriptor.value === value) |
| return key; |
| } |
| } |
| |
| return null; |
| } |
| |
| static _prototypeForType(type) |
| { |
| switch (type) { |
| case WI.Recording.Type.Canvas2D: |
| return CanvasRenderingContext2D.prototype; |
| case WI.Recording.Type.CanvasBitmapRenderer: |
| if (window.ImageBitmapRenderingContext) |
| return ImageBitmapRenderingContext.prototype; |
| break; |
| case WI.Recording.Type.CanvasWebGL: |
| if (window.WebGLRenderingContext) |
| return WebGLRenderingContext.prototype; |
| break; |
| case WI.Recording.Type.CanvasWebGL2: |
| if (window.WebGL2RenderingContext) |
| return WebGL2RenderingContext.prototype; |
| break; |
| } |
| |
| WI.reportInternalError("Unknown recording type: " + type); |
| return null; |
| } |
| |
| // Public |
| |
| get name() { return this._name; } |
| get parameters() { return this._parameters; } |
| get swizzleTypes() { return this._payloadSwizzleTypes; } |
| get trace() { return this._trace; } |
| get snapshot() { return this._snapshot; } |
| get valid() { return this._valid; } |
| 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; } |
| |
| get ready() |
| { |
| return this._swizzled && this._processed; |
| } |
| |
| process(recording, context, states, {lastAction} = {}) |
| { |
| console.assert(this._swizzled, "You must swizzle() before you can process()."); |
| console.assert(!this._processed, "You should only process() once."); |
| |
| this._processed = true; |
| |
| if (recording.type === WI.Recording.Type.CanvasWebGL || recording.type === WI.Recording.Type.CanvasWebGL2) { |
| // We add each RecordingAction to the list of visualActionIndexes after it is processed. |
| if (this._valid && this._isVisual) { |
| let contentBefore = recording.visualActionIndexes.length ? recording.actions[recording.visualActionIndexes.lastValue].snapshot : recording.initialState.content; |
| if (this._snapshot === contentBefore) |
| this._warning = WI.UIString("This action causes no visual change"); |
| } |
| return; |
| } |
| |
| function getContent() { |
| if (context instanceof CanvasRenderingContext2D) |
| return context.getImageData(0, 0, context.canvas.width, context.canvas.height).data; |
| |
| if ((window.WebGLRenderingContext && context instanceof WebGLRenderingContext) || (window.WebGL2RenderingContext && context instanceof WebGL2RenderingContext)) { |
| let pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4); |
| context.readPixels(0, 0, context.canvas.width, context.canvas.height, context.RGBA, context.UNSIGNED_BYTE, pixels); |
| return pixels; |
| } |
| |
| if (context.canvas instanceof HTMLCanvasElement) |
| return [context.canvas.toDataURL()]; |
| |
| console.assert("Unknown context type", context); |
| return []; |
| } |
| |
| let contentBefore = null; |
| let shouldCheckHasVisualEffect = this._valid && this._isVisual; |
| if (shouldCheckHasVisualEffect) |
| contentBefore = getContent(); |
| |
| this.apply(context); |
| |
| if (shouldCheckHasVisualEffect) { |
| let contentAfter = getContent(); |
| if (Array.shallowEqual(contentBefore, contentAfter)) |
| this._warning = WI.UIString("This action causes no visual change"); |
| } |
| |
| if (recording.type === WI.Recording.Type.Canvas2D) { |
| let currentState = WI.RecordingState.fromContext(recording.type, context, {source: this}); |
| console.assert(currentState); |
| |
| if (this.name === "save") |
| states.push(currentState); |
| else if (this.name === "restore") |
| states.pop(); |
| |
| this._states = states.slice(); |
| this._states.push(currentState); |
| |
| let lastState = null; |
| if (lastAction) { |
| lastState = lastAction.states.lastValue; |
| for (let [name, value] of currentState) { |
| let previousValue = lastState.get(name); |
| if (value !== previousValue && !Object.shallowEqual(value, previousValue)) |
| this._stateModifiers.add(name); |
| } |
| } |
| |
| if (WI.ImageUtilities.supportsCanvasPathDebugging()) { |
| let currentX = currentState.get("currentX"); |
| let invalidX = (currentX < 0 || currentX >= context.canvas.width) && (!lastState || currentX !== lastState.get("currentX")); |
| |
| let currentY = currentState.get("currentY"); |
| let invalidY = (currentY < 0 || currentY >= context.canvas.height) && (!lastState || currentY !== lastState.get("currentY")); |
| |
| if (invalidX || invalidY) |
| this._warning = WI.UIString("This action moves the path outside the visible area"); |
| } |
| } |
| } |
| |
| async swizzle(recording, lastAction) |
| { |
| console.assert(!this._swizzled, "You should only swizzle() once."); |
| |
| if (!this._valid) { |
| this._swizzled = true; |
| return; |
| } |
| |
| let swizzleParameter = (item, index) => { |
| return recording.swizzle(item, this._payloadSwizzleTypes[index]); |
| }; |
| |
| let swizzlePromises = [ |
| recording.swizzle(this._payloadName, WI.Recording.Swizzle.String), |
| Promise.all(this._payloadParameters.map(swizzleParameter)), |
| ]; |
| |
| if (!isNaN(this._payloadTrace)) |
| swizzlePromises.push(recording.swizzle(this._payloadTrace, WI.Recording.Swizzle.CallStack)) |
| else { |
| // COMPATIBILITY (iOS 12.1): "trace" was sent as an array of call frames instead of a single call stack |
| swizzlePromises.push(Promise.all(this._payloadTrace.map((item) => recording.swizzle(item, WI.Recording.Swizzle.CallFrame)))); |
| } |
| |
| if (this._payloadSnapshot >= 0) |
| swizzlePromises.push(recording.swizzle(this._payloadSnapshot, WI.Recording.Swizzle.String)); |
| |
| let [name, parameters, callFrames, snapshot] = await Promise.all(swizzlePromises); |
| this._name = name; |
| this._parameters = parameters; |
| this._trace = callFrames; |
| if (this._payloadSnapshot >= 0) |
| this._snapshot = snapshot; |
| |
| if (recording.type === WI.Recording.Type.Canvas2D || recording.type === WI.Recording.Type.CanvasBitmapRenderer || recording.type === WI.Recording.Type.CanvasWebGL || recording.type === WI.Recording.Type.CanvasWebGL2) { |
| if (this._name === "width" || this._name === "height") { |
| this._contextReplacer = "canvas"; |
| this._isFunction = false; |
| this._isGetter = !this._parameters.length; |
| this._isVisual = !this._isGetter; |
| } |
| |
| // FIXME: <https://webkit.org/b/180833> |
| } |
| |
| if (!this._contextReplacer) { |
| this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name); |
| this._isGetter = !this._isFunction && !this._parameters.length; |
| |
| if (this._snapshot) |
| this._isVisual = true; |
| else { |
| 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.synthesizeWarning(WI.UIString("\u0022%s\u0022 is not valid for %s").format(name, prototype.constructor.name)); |
| } |
| } |
| } |
| |
| if (this._valid) { |
| let parametersSpecified = this._parameters.every((parameter) => parameter !== undefined); |
| let parametersCanBeSwizzled = this._payloadSwizzleTypes.every((swizzleType) => swizzleType !== WI.Recording.Swizzle.None); |
| if (!parametersSpecified || !parametersCanBeSwizzled) |
| this.markInvalid(); |
| } |
| |
| if (this._valid) { |
| let stateModifiers = WI.RecordingAction._stateModifiers[recording.type]; |
| if (stateModifiers) { |
| this._stateModifiers.add(this._name); |
| let modifiedByAction = stateModifiers[this._name] || []; |
| for (let item of modifiedByAction) |
| this._stateModifiers.add(item); |
| } |
| } |
| |
| this._swizzled = true; |
| } |
| |
| apply(context, options = {}) |
| { |
| console.assert(this._swizzled, "You must swizzle() before you can apply()."); |
| console.assert(this._processed, "You must process() before you can apply()."); |
| |
| if (!this.valid) |
| return; |
| |
| try { |
| let name = options.nameOverride || this._name; |
| |
| if (this._contextReplacer) |
| context = context[this._contextReplacer]; |
| |
| if (this.isFunction) |
| context[name](...this._parameters); |
| else { |
| if (this.isGetter) |
| context[name]; |
| else |
| context[name] = this._parameters[0]; |
| } |
| } catch { |
| this.markInvalid(); |
| |
| WI.Recording.synthesizeWarning(WI.UIString("\u0022%s\u0022 threw an error").format(this._name)); |
| } |
| } |
| |
| markInvalid() |
| { |
| if (!this._valid) |
| return; |
| |
| this._valid = false; |
| |
| this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged); |
| } |
| |
| getColorParameters() |
| { |
| switch (this._name) { |
| // 2D |
| case "fillStyle": |
| case "strokeStyle": |
| case "shadowColor": |
| // 2D (non-standard, legacy) |
| case "setFillColor": |
| case "setStrokeColor": |
| // WebGL |
| case "blendColor": |
| case "clearColor": |
| case "colorMask": |
| return this._parameters; |
| |
| // 2D (non-standard, legacy) |
| case "setShadow": |
| return this._parameters.slice(3); |
| } |
| |
| return []; |
| } |
| |
| getImageParameters() |
| { |
| switch (this._name) { |
| // 2D |
| case "createImageData": |
| case "createPattern": |
| case "drawImage": |
| case "fillStyle": |
| case "putImageData": |
| case "strokeStyle": |
| // 2D (non-standard) |
| case "drawImageFromRect": |
| // BitmapRenderer |
| case "transferFromImageBitmap": |
| return this._parameters.slice(0, 1); |
| |
| // WebGL |
| case "texImage2D": |
| case "texSubImage2D": |
| case "compressedTexImage2D": |
| return [this._parameters.lastValue]; |
| } |
| |
| return []; |
| } |
| |
| toJSON() |
| { |
| let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadTrace]; |
| if (this._payloadSnapshot >= 0) |
| json.push(this._payloadSnapshot); |
| return json; |
| } |
| }; |
| |
| WI.RecordingAction.Event = { |
| ValidityChanged: "recording-action-marked-invalid", |
| }; |
| |
| WI.RecordingAction._constantIndexes = { |
| [WI.Recording.Type.CanvasWebGL]: { |
| "activeTexture": true, |
| "bindBuffer": true, |
| "bindFramebuffer": true, |
| "bindRenderbuffer": true, |
| "bindTexture": true, |
| "blendEquation": true, |
| "blendEquationSeparate": true, |
| "blendFunc": true, |
| "blendFuncSeparate": true, |
| "bufferData": [0, 2], |
| "bufferSubData": [0], |
| "checkFramebufferStatus": true, |
| "compressedTexImage2D": [0, 2], |
| "compressedTexSubImage2D": [0], |
| "copyTexImage2D": [0, 2], |
| "copyTexSubImage2D": [0], |
| "createShader": true, |
| "cullFace": true, |
| "depthFunc": true, |
| "disable": true, |
| "drawArrays": [0], |
| "drawElements": [0, 2], |
| "enable": true, |
| "framebufferRenderbuffer": true, |
| "framebufferTexture2D": [0, 1, 2], |
| "frontFace": true, |
| "generateMipmap": true, |
| "getBufferParameter": true, |
| "getFramebufferAttachmentParameter": true, |
| "getParameter": true, |
| "getProgramParameter": true, |
| "getRenderbufferParameter": true, |
| "getShaderParameter": true, |
| "getShaderPrecisionFormat": true, |
| "getTexParameter": true, |
| "getVertexAttrib": [1], |
| "getVertexAttribOffset": [1], |
| "hint": true, |
| "isEnabled": true, |
| "pixelStorei": [0], |
| "readPixels": [4, 5], |
| "renderbufferStorage": [0, 1], |
| "stencilFunc": [0], |
| "stencilFuncSeparate": [0, 1], |
| "stencilMaskSeparate": [0], |
| "stencilOp": true, |
| "stencilOpSeparate": true, |
| "texImage2D": { |
| 5: [0, 2, 3, 4], |
| 6: [0, 2, 3, 4], |
| 8: [0, 2, 6, 7], |
| 9: [0, 2, 6, 7], |
| }, |
| "texParameterf": [0, 1], |
| "texParameteri": [0, 1], |
| "texSubImage2D": { |
| 6: [0, 4, 5], |
| 7: [0, 4, 5], |
| 8: [0, 6, 7], |
| 9: [0, 6, 7], |
| }, |
| "vertexAttribPointer": [2], |
| }, |
| [WI.Recording.Type.CanvasWebGL2]: { |
| "activeTexture": true, |
| "beginQuery": [0], |
| "beginTransformFeedback": true, |
| "bindBuffer": true, |
| "bindBufferBase": [0], |
| "bindBufferRange": [0], |
| "bindFramebuffer": true, |
| "bindRenderbuffer": true, |
| "bindTexture": true, |
| "bindTransformFeedback": [0], |
| "blendEquation": true, |
| "blendEquationSeparate": true, |
| "blendFunc": true, |
| "blendFuncSeparate": true, |
| "blitFramebuffer": [10], |
| "bufferData": [0, 2], |
| "bufferSubData": [0], |
| "checkFramebufferStatus": true, |
| "clearBufferfi": [0], |
| "clearBufferfv": [0], |
| "clearBufferiv": [0], |
| "clearBufferuiv": [0], |
| "compressedTexImage2D": [0, 2], |
| "compressedTexSubImage2D": [0], |
| "compressedTexSubImage3D": [0], |
| "copyBufferSubData": [0, 1], |
| "copyTexImage2D": [0, 2], |
| "copyTexSubImage2D": [0], |
| "copyTexSubImage3D": [0], |
| "createShader": true, |
| "cullFace": true, |
| "depthFunc": true, |
| "disable": true, |
| "drawArrays": [0], |
| "drawArraysInstanced": [0], |
| "drawBuffers": true, |
| "drawElements": [0, 2], |
| "drawElementsInstanced": [0, 2], |
| "drawRangeElements": [0, 4], |
| "enable": true, |
| "endQuery": true, |
| "fenceSync": [0], |
| "framebufferRenderbuffer": true, |
| "framebufferTexture2D": [0, 1, 2], |
| "framebufferTextureLayer": [0, 1], |
| "frontFace": true, |
| "generateMipmap": true, |
| "getActiveUniformBlockParameter": [2], |
| "getActiveUniforms": [2], |
| "getBufferParameter": true, |
| "getBufferSubData": [0], |
| "getFramebufferAttachmentParameter": true, |
| "getIndexedParameter": [0], |
| "getInternalformatParameter": true, |
| "getParameter": true, |
| "getProgramParameter": true, |
| "getQuery": true, |
| "getQueryParameter": [1], |
| "getRenderbufferParameter": true, |
| "getSamplerParameter": [1], |
| "getShaderParameter": true, |
| "getShaderPrecisionFormat": true, |
| "getSyncParameter": [1], |
| "getTexParameter": true, |
| "getVertexAttrib": [1], |
| "getVertexAttribOffset": [1], |
| "hint": true, |
| "invalidateFramebuffer": [0, 1], |
| "invalidateSubFramebuffer": [0, 1], |
| "isEnabled": true, |
| "pixelStorei": [0], |
| "readBuffer": true, |
| "readPixels": [4, 5], |
| "renderbufferStorage": [0, 1], |
| "renderbufferStorageMultisample": [0, 2], |
| "samplerParameterf": [1], |
| "samplerParameteri": [1], |
| "stencilFunc": [0], |
| "stencilFuncSeparate": [0, 1], |
| "stencilMaskSeparate": [0], |
| "stencilOp": true, |
| "stencilOpSeparate": true, |
| "texImage2D": { |
| 5: [0, 2, 3, 4], |
| 6: [0, 2, 3, 4], |
| 8: [0, 2, 6, 7], |
| 9: [0, 2, 6, 7], |
| 10: [0, 2, 6, 7], |
| 11: [0, 2, 7, 8], |
| }, |
| "texParameterf": [0, 1], |
| "texParameteri": [0, 1], |
| "texStorage2D":[0, 2], |
| "texSubImage2D": { |
| 6: [0, 4, 5], |
| 7: [0, 4, 5], |
| 8: [0, 6, 7], |
| 9: [0, 6, 7], |
| 10: [0, 6, 7], |
| 11: [0, 8, 9], |
| 12: [0, 8, 9], |
| }, |
| "transformFeedbackVaryings": [2], |
| "vertexAttribIPointer": [2], |
| "vertexAttribPointer": [2], |
| }, |
| }; |
| |
| WI.RecordingAction._visualNames = { |
| [WI.Recording.Type.Canvas2D]: new Set([ |
| "clearRect", |
| "drawFocusIfNeeded", |
| "drawImage", |
| "drawImageFromRect", |
| "fill", |
| "fillRect", |
| "fillText", |
| "putImageData", |
| "stroke", |
| "strokeRect", |
| "strokeText", |
| ]), |
| [WI.Recording.Type.CanvasBitmapRenderer]: new Set([ |
| "transferFromImageBitmap", |
| ]), |
| [WI.Recording.Type.CanvasWebGL]: new Set([ |
| "clear", |
| "drawArrays", |
| "drawElements", |
| ]), |
| [WI.Recording.Type.CanvasWebGL2]: new Set([ |
| "clear", |
| "drawArrays", |
| "drawArraysInstanced", |
| "drawElements", |
| "drawElementsInstanced", |
| ]), |
| }; |
| |
| WI.RecordingAction._stateModifiers = { |
| [WI.Recording.Type.Canvas2D]: { |
| arc: ["currentX", "currentY"], |
| arcTo: ["currentX", "currentY"], |
| beginPath: ["currentX", "currentY"], |
| bezierCurveTo: ["currentX", "currentY"], |
| clearShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"], |
| closePath: ["currentX", "currentY"], |
| ellipse: ["currentX", "currentY"], |
| lineTo: ["currentX", "currentY"], |
| moveTo: ["currentX", "currentY"], |
| quadraticCurveTo: ["currentX", "currentY"], |
| rect: ["currentX", "currentY"], |
| resetTransform: ["transform"], |
| rotate: ["transform"], |
| scale: ["transform"], |
| setAlpha: ["globalAlpha"], |
| setCompositeOperation: ["globalCompositeOperation"], |
| setFillColor: ["fillStyle"], |
| setLineCap: ["lineCap"], |
| setLineJoin: ["lineJoin"], |
| setLineWidth: ["lineWidth"], |
| setMiterLimit: ["miterLimit"], |
| setShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"], |
| setStrokeColor: ["strokeStyle"], |
| setTransform: ["transform"], |
| translate: ["transform"], |
| }, |
| }; |