/* * 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.RecordingContentView = class RecordingContentView extends WI.ContentView { constructor(representedObject) { console.assert(representedObject instanceof WI.Recording); super(representedObject); let isCanvas2D = this.representedObject.type === WI.Recording.Type.Canvas2D; let isCanvasBitmapRenderer = this.representedObject.type === WI.Recording.Type.CanvasBitmapRenderer; let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL; let isCanvasWebGL2 = this.representedObject.type === WI.Recording.Type.CanvasWebGL2; this._index = NaN; this._action = null; this._snapshots = []; this._initialContent = null; this._generateContentThrottler = new Throttler(() => { if (isCanvas2D) this._generateContentCanvas2D(this._index); else if (isCanvasBitmapRenderer || isCanvasWebGL || isCanvasWebGL2) this._generateContentFromSnapshot(this._index); }, 200); this.element.classList.add("recording", this.representedObject.type); if (isCanvas2D || isCanvasBitmapRenderer || isCanvasWebGL || isCanvasWebGL2) { if (isCanvas2D) { this._pathContext = null; this._showPathButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-path", WI.UIString("Show Path"), WI.UIString("Hide Path"), "Images/Path.svg", 16, 16); this._showPathButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High; this._showPathButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showPathButtonClicked, this); this._showPathButtonNavigationItem.activated = !!WI.settings.showCanvasPath.value; } this._showGridButtonNavigationItem = new WI.ActivateButtonNavigationItem("show-grid", WI.repeatedUIString.showTransparencyGridTooltip(), WI.UIString("Hide transparency grid"), "Images/NavigationItemCheckers.svg", 13, 13); this._showGridButtonNavigationItem.visibilityPriority = WI.NavigationItem.VisibilityPriority.High; this._showGridButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._showGridButtonClicked, this); this._showGridButtonNavigationItem.activated = !!WI.settings.showImageGrid.value; if (isCanvas2D && WI.FileUtilities.canSave(WI.FileUtilities.SaveMode.FileVariants)) this._saveMode = WI.FileUtilities.SaveMode.FileVariants; else if (WI.FileUtilities.canSave(WI.FileUtilities.SaveMode.SingleFile)) this._saveMode = WI.FileUtilities.SaveMode.SingleFile; else this._saveMode = null; this._exportButtonNavigationItem = new WI.ButtonNavigationItem("export", WI.UIString("Export"), "Images/Export.svg", 15, 15); this._exportButtonNavigationItem.tooltip = WI.UIString("Export (%s)").format(WI.saveKeyboardShortcut.displayName); this._exportButtonNavigationItem.buttonStyle = WI.ButtonNavigationItem.Style.ImageAndText; this._exportButtonNavigationItem.enabled = this._saveMode; this._exportButtonNavigationItem.addEventListener(WI.ButtonNavigationItem.Event.Clicked, this._handleExportNavigationItemClicked, this); this._updateExportButton(); } } // Public get navigationItems() { let isCanvas2D = this.representedObject.type === WI.Recording.Type.Canvas2D; let isCanvasBitmapRenderer = this.representedObject.type === WI.Recording.Type.CanvasBitmapRenderer; let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL; let isCanvasWebGL2 = this.representedObject.type === WI.Recording.Type.CanvasWebGL2; if (!isCanvas2D && !isCanvasBitmapRenderer && !isCanvasWebGL && !isCanvasWebGL2) return []; let navigationItems = [this._exportButtonNavigationItem, new WI.DividerNavigationItem]; if (isCanvas2D) navigationItems.push(this._showPathButtonNavigationItem); navigationItems.push(this._showGridButtonNavigationItem); return navigationItems; } get supplementalRepresentedObjects() { return this._action ? [this._action] : []; } updateActionIndex(index) { if (!this.representedObject) return; if (this._index === index) return; console.assert(index >= 0 && index < this.representedObject.actions.length); if (index < 0 || index >= this.representedObject.actions.length) return; this._index = index; this._updateSliderValue(); if (this.didInitialLayout) this._generateContentThrottler.fire(); this._action = this.representedObject.actions[this._index]; this.dispatchEventToListeners(WI.ContentView.Event.SupplementalRepresentedObjectsDidChange); } attached() { super.attached(); let isCanvas2D = this.representedObject.type === WI.Recording.Type.Canvas2D; let isCanvasBitmapRenderer = this.representedObject.type === WI.Recording.Type.CanvasBitmapRenderer; let isCanvasWebGL = this.representedObject.type === WI.Recording.Type.CanvasWebGL; let isCanvasWebGL2 = this.representedObject.type === WI.Recording.Type.CanvasWebGL2; if (isCanvas2D || isCanvasBitmapRenderer || isCanvasWebGL || isCanvasWebGL2) { if (isCanvas2D) this._updateCanvasPath(); this._updateImageGrid(); } } detached() { this._generateContentThrottler.cancel(); super.detached(); } // Protected get supportsSave() { return !!this._saveMode; } get saveMode() { return this._saveMode; } get saveData() { return {customSaveHandler: () => { this._export(); }}; } initialLayout() { let previewHeader = this.element.appendChild(document.createElement("header")); let sliderContainer = previewHeader.appendChild(document.createElement("div")); sliderContainer.className = "slider-container"; this._previewContainer = this.element.appendChild(document.createElement("div")); this._previewContainer.className = "preview-container"; this._sliderValueElement = sliderContainer.appendChild(document.createElement("div")); this._sliderValueElement.className = "slider-value"; this._sliderElement = sliderContainer.appendChild(document.createElement("input")); this._sliderElement.addEventListener("input", this._sliderChanged.bind(this)); this._sliderElement.type = "range"; this._sliderElement.min = 0; this._sliderElement.max = 0; if (!this.representedObject.ready) { this.representedObject.addEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); if (!this.representedObject.processing) this.representedObject.startProcessing(); } this._updateSliderValue(); if (!isNaN(this._index)) this._generateContentThrottler.fire(); } // Private _export() { if (this._saveMode === WI.FileUtilities.SaveMode.SingleFile) { this._exportRecording(); return; } let filename = this.representedObject.displayName; let data = []; data.push({ displayType: WI.UIString("Recording"), content: JSON.stringify(this.representedObject.toJSON()), suggestedName: filename + ".recording", }); if (this.representedObject.type === WI.Recording.Type.Canvas2D && this.representedObject.ready) { data.push({ displayType: WI.UIString("Reduction"), content: this.representedObject.toHTML(), suggestedName: filename + ".html", }); } const forceSaveAs = true; WI.FileUtilities.save(WI.FileUtilities.SaveMode.FileVariants, data, forceSaveAs); } _exportRecording() { let filename = this.representedObject.displayName; const forceSaveAs = true; WI.FileUtilities.save(WI.FileUtilities.SaveMode.SingleFile, { content: JSON.stringify(this.representedObject.toJSON()), suggestedName: filename + ".recording", }, forceSaveAs); } _exportReduction() { console.assert(this.representedObject.ready); let filename = this.representedObject.displayName; const forceSaveAs = true; WI.FileUtilities.save(WI.FileUtilities.SaveMode.SingleFile, { content: this.representedObject.toHTML(), suggestedName: filename + ".html", }, forceSaveAs); } _generateContentCanvas2D(index) { let imageLoad = (event) => { // Loading took too long and the current action index has already changed. if (index !== this._index) return; this._generateContentCanvas2D(index); }; let initialState = this.representedObject.initialState; if (initialState.content && !this._initialContent) { this._initialContent = new Image; this._initialContent.src = initialState.content; this._initialContent.addEventListener("load", imageLoad); return; } let snapshotIndex = Math.floor(index / WI.RecordingContentView.SnapshotInterval); let snapshot = this._snapshots[snapshotIndex]; let indexOfLastBeginPathAction = Infinity; let actions = this.representedObject.actions; let applyActions = (from, to, callback) => { 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); } for (let state of snapshot.states) { state.apply(this.representedObject.type, snapshot.context); ++saveCount; snapshot.context.save(); } 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 (WI.settings.showCanvasPath.value && indexOfLastBeginPathAction <= to) { if (!this._pathContext) { let pathCanvas = document.createElement("canvas"); pathCanvas.classList.add("path"); this._pathContext = pathCanvas.getContext("2d"); } this._pathContext.canvas.width = snapshot.element.width; this._pathContext.canvas.height = snapshot.element.height; this._pathContext.clearRect(0, 0, snapshot.element.width, snapshot.element.height); this._pathContext.save(); this._pathContext.fillStyle = "hsla(0, 0%, 100%, 0.75)"; this._pathContext.fillRect(0, 0, snapshot.element.width, snapshot.element.height); 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; } return false; } for (let i = indexOfLastBeginPathAction; i <= to; ++i) { if (!actionModifiesPath(actions[i])) continue; lastPathPoint = {x: this._pathContext.currentX, y: this._pathContext.currentY}; if (i === indexOfLastBeginPathAction) this._pathContext.setTransform(snapshot.context.getTransform()); let isMoveTo = actions[i].name === "moveTo"; this._pathContext.lineWidth = isMoveTo ? 0.5 : 1; this._pathContext.setLineDash(isMoveTo ? [5, 5] : []); this._pathContext.strokeStyle = i === to ? "red" : "black"; this._pathContext.beginPath(); if (!isEmptyObject(lastPathPoint)) this._pathContext.moveTo(lastPathPoint.x, lastPathPoint.y); if (actions[i].name === "closePath" && !isEmptyObject(subPathStartPoint)) { this._pathContext.lineTo(subPathStartPoint.x, subPathStartPoint.y); subPathStartPoint = {}; } else { actions[i].apply(this._pathContext, {nameOverride: isMoveTo ? "lineTo" : null}); if (isMoveTo) subPathStartPoint = {x: this._pathContext.currentX, y: this._pathContext.currentY}; } this._pathContext.stroke(); } this._pathContext.restore(); this._previewContainer.appendChild(this._pathContext.canvas); } else if (this._pathContext) this._pathContext.canvas.remove(); snapshot.context.restore(); while (saveCount-- > 0) snapshot.context.restore(); }; if (!snapshot) { snapshot = this._snapshots[snapshotIndex] = {}; snapshot.index = snapshotIndex * WI.RecordingContentView.SnapshotInterval; while (snapshot.index && actions[snapshot.index].name !== "beginPath") --snapshot.index; snapshot.context = this.representedObject.createContext(); snapshot.element = snapshot.context.canvas; let lastSnapshotIndex = snapshotIndex; while (--lastSnapshotIndex >= 0) { if (this._snapshots[lastSnapshotIndex]) break; } let startIndex = 0; if (lastSnapshotIndex < 0) { snapshot.content = this._initialContent; snapshot.states = actions[0].states; snapshot.attributes = Object.shallowCopy(initialState.attributes); } else { 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); if (snapshot.index > 0) snapshot.states = actions[snapshot.index - 1].states; snapshot.content = new Image; snapshot.content.src = snapshot.element.toDataURL(); snapshot.content.addEventListener("load", imageLoad); return; } this._previewContainer.removeChildren(); if (WI.settings.showCanvasPath.value) { indexOfLastBeginPathAction = this._index; while (indexOfLastBeginPathAction > snapshot.index && actions[indexOfLastBeginPathAction].name !== "beginPath") --indexOfLastBeginPathAction; } applyActions(snapshot.index, this._index); this._previewContainer.insertAdjacentElement("afterbegin", snapshot.element); this._updateImageGrid(); } _generateContentFromSnapshot(index) { let imageLoad = (event) => { // Loading took too long and the current action index has already changed. if (index !== this._index) return; this._generateContentFromSnapshot(index); }; let initialState = this.representedObject.initialState; if (initialState.content && !this._initialContent) { this._initialContent = new Image; this._initialContent.src = initialState.content; this._initialContent.addEventListener("load", imageLoad); return; } let actions = this.representedObject.actions; let visualIndex = index; while (!actions[visualIndex].isVisual && !(actions[visualIndex] instanceof WI.RecordingInitialStateAction)) visualIndex--; let snapshot = this._snapshots[visualIndex]; if (!snapshot) { if (actions[visualIndex].snapshot) { snapshot = this._snapshots[visualIndex] = {element: new Image}; snapshot.element.src = actions[visualIndex].snapshot; snapshot.element.addEventListener("load", imageLoad); return; } if (actions[visualIndex] instanceof WI.RecordingInitialStateAction) snapshot = this._snapshots[visualIndex] = {element: this._initialContent}; } if (snapshot) { this._previewContainer.removeChildren(); this._previewContainer.appendChild(snapshot.element); this._updateImageGrid(); } } _updateExportButton() { if (this._saveMode !== WI.FileUtilities.SaveMode.SingleFile) return; if (this.representedObject.type === WI.Recording.Type.Canvas2D && this.representedObject.ready) this._exportButtonNavigationItem.tooltip = WI.UIString("Export recording (%s)\nShift-click to export a HTML reduction").format(WI.saveKeyboardShortcut.displayName); else this._exportButtonNavigationItem.tooltip = WI.UIString("Export recording (%s)").format(WI.saveKeyboardShortcut.displayName); } _updateCanvasPath() { let activated = WI.settings.showCanvasPath.value; if (this._showPathButtonNavigationItem.activated !== activated) this._generateContentCanvas2D(this._index); this._showPathButtonNavigationItem.activated = activated; } _updateImageGrid() { let activated = WI.settings.showImageGrid.value; this._showGridButtonNavigationItem.activated = activated; if (this.didInitialLayout && !isNaN(this._index)) this._previewContainer.firstElementChild.classList.toggle("show-grid", activated); } _updateSliderValue() { if (!this._sliderElement) return; let visualActionIndexes = this.representedObject.visualActionIndexes; let visualActionIndex = 0; if (this._index > 0) { while (visualActionIndex < visualActionIndexes.length && visualActionIndexes[visualActionIndex] <= this._index) visualActionIndex++; } this._sliderElement.value = visualActionIndex; this._sliderElement.max = visualActionIndexes.length; this._sliderValueElement.textContent = WI.UIString("%d of %d").format(visualActionIndex, visualActionIndexes.length); } _showPathButtonClicked(event) { WI.settings.showCanvasPath.value = !this._showPathButtonNavigationItem.activated; this._updateCanvasPath(); } _showGridButtonClicked(event) { WI.settings.showImageGrid.value = !this._showGridButtonNavigationItem.activated; this._updateImageGrid(); } _handleExportNavigationItemClicked(event) { if (this._saveMode === WI.FileUtilities.SaveMode.SingleFile && this.representedObject.type === WI.Recording.Type.Canvas2D && this.representedObject.ready && event.data.nativeEvent.shiftKey) { this._exportReduction(); return; } this._export(); } _sliderChanged() { let index = 0; let visualActionIndex = parseInt(this._sliderElement.value) - 1; if (visualActionIndex !== -1) index = this.representedObject.visualActionIndexes[visualActionIndex]; this.updateActionIndex(index); } _handleRecordingProcessedAction(event) { this._updateExportButton(); this._updateSliderValue(); if (this.representedObject.ready) this.representedObject.removeEventListener(WI.Recording.Event.ProcessedAction, this._handleRecordingProcessedAction, this); } }; WI.RecordingContentView.SnapshotInterval = 5000;