From 2b79008596351f6948d8de80c7887446d97b068c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 15 May 2020 00:03:41 -0700
Subject: renamed new_fields to fields

---
 src/client/views/collections/CollectionCarouselView.tsx | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

(limited to 'src/client/views/collections/CollectionCarouselView.tsx')

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index a04136e51..39bb9bc23 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -2,18 +2,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { observable, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { documentSchema, collectionSchema } from '../../../new_fields/documentSchemas';
-import { makeInterface } from '../../../new_fields/Schema';
-import { NumCast, StrCast, ScriptCast, Cast } from '../../../new_fields/Types';
+import { documentSchema, collectionSchema } from '../../../fields/documentSchemas';
+import { makeInterface } from '../../../fields/Schema';
+import { NumCast, StrCast, ScriptCast, Cast } from '../../../fields/Types';
 import { DragManager } from '../../util/DragManager';
 import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
 import "./CollectionCarouselView.scss";
 import { CollectionSubView } from './CollectionSubView';
 import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
-import { Doc } from '../../../new_fields/Doc';
+import { Doc } from '../../../fields/Doc';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { ContextMenu } from '../ContextMenu';
-import { ObjectField } from '../../../new_fields/ObjectField';
+import { ObjectField } from '../../../fields/ObjectField';
 import { returnFalse } from '../../../Utils';
 
 type CarouselDocument = makeInterface<[typeof documentSchema, typeof collectionSchema]>;
-- 
cgit v1.2.3-70-g09d2


From 61dcd6a0e23384e68a7d7f2f9e0143741f5cbf56 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 15 May 2020 17:36:39 -0400
Subject: moving toward a proper slide transition system

---
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/MainView.tsx                      |   5 +-
 .../views/collections/CollectionCarouselView.tsx   |   5 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 118 ++++++++++-----------
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  58 +++++++++-
 src/fields/ScriptField.ts                          |   8 +-
 7 files changed, 128 insertions(+), 72 deletions(-)

(limited to 'src/client/views/collections/CollectionCarouselView.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index fc7c16296..6639f1cce 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -483,7 +483,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown} >
                     <span style={{ width: "calc(100% - 25px)", display: "inline-block" }}>{`${this.selectionTitle}`}</span>
                 </div>
-            </>
+            </>;
 
         bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
         bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 978cf7868..f58313f06 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
-    faTrashAlt, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
+    faTrashAlt, faAngleRight, faBell, faTrash, faCamera, faExpand, faCaretDown, faCaretLeft, faCaretRight, faCaretSquareDown, faCaretSquareRight, faArrowsAltH, faPlus, faMinus,
     faTerminal, faToggleOn, faFile as fileSolid, faExternalLinkAlt, faLocationArrow, faSearch, faFileDownload, faStop, faCalculator, faWindowMaximize, faAddressCard,
     faQuestionCircle, faArrowLeft, faArrowRight, faArrowDown, faArrowUp, faBolt, faBullseye, faCaretUp, faCat, faCheck, faChevronRight, faClipboard, faClone, faCloudUploadAlt,
     faCommentAlt, faCompressArrowsAlt, faCut, faEllipsisV, faEraser, faExclamation, faFileAlt, faFileAudio, faFilePdf, faFilm, faFilter, faFont, faGlobeAsia, faHighlighter,
@@ -121,6 +121,8 @@ export class MainView extends React.Component {
         library.add(faCamera);
         library.add(faExpand);
         library.add(faCaretDown);
+        library.add(faCaretUp);
+        library.add(faCaretLeft);
         library.add(faCaretRight);
         library.add(faCaretSquareDown);
         library.add(faCaretSquareRight);
@@ -168,7 +170,6 @@ export class MainView extends React.Component {
         library.add(faThumbtack);
         library.add(faLongArrowAltRight);
         library.add(faCheck);
-        library.add(faCaretUp);
         library.add(faFilter);
         library.add(faBullseye);
         library.add(faArrowLeft);
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 39bb9bc23..f65a89422 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -9,7 +9,6 @@ import { DragManager } from '../../util/DragManager';
 import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
 import "./CollectionCarouselView.scss";
 import { CollectionSubView } from './CollectionSubView';
-import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
 import { Doc } from '../../../fields/Doc';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { ContextMenu } from '../ContextMenu';
@@ -76,10 +75,10 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
     @computed get buttons() {
         return <>
             <div key="back" className="carouselView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.goback}>
-                <FontAwesomeIcon icon={faCaretLeft} size={"2x"} />
+                <FontAwesomeIcon icon={"caret-left"} size={"2x"} />
             </div>
             <div key="fwd" className="carouselView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={this.advance}>
-                <FontAwesomeIcon icon={faCaretRight} size={"2x"} />
+                <FontAwesomeIcon icon={"caret-right"} size={"2x"} />
             </div>
         </>;
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 3860ce2d7..a4fd5384f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -25,6 +25,7 @@ export interface ViewDefBounds {
     fontSize?: number;
     highlight?: boolean;
     color?: string;
+    opacity?: number;
     replica?: string;
     pair?: { layout: Doc, data?: Doc };
 }
@@ -37,6 +38,7 @@ export interface PoolData {
     width?: number;
     height?: number;
     color?: string;
+    opacity?: number;
     transition?: string;
     highlight?: boolean;
     replica: string;
@@ -416,7 +418,7 @@ function normalizeResults(
                 height: newPosRaw.height! * scale,
                 pair: ele[1].pair
             };
-            poolData.set(newPos.pair.layout[Id] + (newPos.replica || ""), { transition: "transform 1s", ...newPos });
+            poolData.set(newPos.pair.layout[Id] + (newPos.replica || ""), { transition: "all 1s", ...newPos });
         }
     });
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bf679309c..751ee03d2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -46,6 +46,7 @@ import React = require("react");
 import { CollectionViewType } from "../CollectionView";
 import { Timeline } from "../../animationtimeline/Timeline";
 import { SnappingManager } from "../../../util/SnappingManager";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -53,6 +54,7 @@ export const panZoomSchema = createSchema({
     _panX: "number",
     _panY: "number",
     scale: "number",
+    timecode: "number",
     arrangeScript: ScriptField,
     arrangeInit: ScriptField,
     useClusters: "boolean",
@@ -124,15 +126,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         this.addDocument(newBox);
     }
     addDocument = (newBox: Doc | Doc[]) => {
-        const timecode = Cast(this.props.Document.timecode, "number", null);
-        if (timecode !== undefined) {
-            ((newBox instanceof Doc) ? [newBox] : newBox).map(doc => {
-                doc["x-indexed"] = new List<number>(numberRange(timecode + 1).map(i => NumCast(doc.x)));
-                doc["y-indexed"] = new List<number>(numberRange(timecode + 1).map(i => NumCast(doc.y)));
-                doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection: this.props.Document });
-                doc.x = ComputedField.MakeInterpolated("x", "timecode");
-                doc.y = ComputedField.MakeInterpolated("y", "timecode");
-            });
+        if (this.Document.timecode !== undefined) {
+            CollectionFreeFormDocumentView.setupKeyframes((newBox instanceof Doc) ? [newBox] : newBox, this.Document.timecode, this.props.Document);
         }
 
         if (newBox instanceof Doc) {
@@ -145,6 +140,25 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             // bcz: deal with clusters
         }
     }
+
+    @undoBatch
+    @action
+    nextKeyframe = (): void => {
+        if (this.props.Document.timecode === undefined) {
+            this.props.Document.timecode = 0;
+            CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0, this.props.Document);
+        }
+        const timecode = NumCast(this.props.Document.timecode);
+        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, timecode);
+        this.props.Document.timecode = Math.max(0, timecode + 1);
+    }
+    @undoBatch
+    @action
+    prevKeyframe = (): void => {
+        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+        this.props.Document.timecode = Math.max(0, NumCast(this.props.Document.timecode) - 1);
+    }
+
     private selectDocuments = (docs: Doc[]) => {
         SelectionManager.DeselectAll();
         docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
@@ -185,8 +199,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                         for (let i = 0; i < droppedDocs.length; i++) {
                             const d = droppedDocs[i];
                             const layoutDoc = Doc.Layout(d);
-                            d.x = x + NumCast(d.x) - dropX;
-                            d.y = y + NumCast(d.y) - dropY;
+                            if (this.Document.timecode !== undefined) {
+                                CollectionFreeFormDocumentView.setValues(this.Document.timecode, d, x + NumCast(d.x) - dropX, y + NumCast(d.y) - dropY, Cast(d.opacity, "number", null));
+                            } else {
+                                d.x = x + NumCast(d.x) - dropX;
+                                d.y = y + NumCast(d.y) - dropY;
+                            }
                             if (!NumCast(layoutDoc._width)) {
                                 layoutDoc._width = 300;
                             }
@@ -953,10 +971,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             return { x: 0, y: 0, transition: "transform 1s", ...result, pair: params.pair, replica: "" };
         }
         const layoutDoc = Doc.Layout(params.pair.layout);
-        const { x, y, z, color, zIndex } = params.pair.layout;
+        const { x, y, opacity } = this.Document.timecode === undefined ? params.pair.layout :
+            CollectionFreeFormDocumentView.getValues(params.pair.layout, this.Document.timecode);
+        if (this.Document.timecode !== undefined) {
+            const time = this.Document.timecode || 0;
+        }
+        const { z, color, zIndex } = params.pair.layout;
         return {
             x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"),
-            transition: StrCast(layoutDoc.transition),
+            transition: StrCast(layoutDoc.transition), opacity: Cast(opacity, "number", null),
             width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number"), pair: params.pair, replica: ""
         };
     }
@@ -1142,39 +1165,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         Doc.toggleNativeDimensions(this.layoutDoc, this.props.ContentScaling(), this.props.NativeWidth(), this.props.NativeHeight());
     }
 
-    @undoBatch
-    @action
-    snaphsotInterpolated = (): void => {
-        if (this.props.Document.timecode === undefined) {
-            this.childDocs.map(doc => {
-                this.props.Document.timecode = 0;
-                doc["x-indexed"] = new List<number>([NumCast(doc.x)]);
-                doc["y-indexed"] = new List<number>([NumCast(doc.y)]);
-                doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection: this.props.Document });
-                doc.x = ComputedField.MakeInterpolated("x", "timecode");
-                doc.y = ComputedField.MakeInterpolated("y", "timecode");
-            });
-        }
-        const timecode = NumCast(this.props.Document.timecode);
-        this.childDocs.map(doc => {
-            const xindexed = Cast(doc['x-indexed'], listSpec("number"), null);
-            const yindexed = Cast(doc['y-indexed'], listSpec("number"), null);
-            xindexed.length <= timecode + 1 && xindexed.push(NumCast(doc.x));
-            yindexed.length <= timecode + 1 && yindexed.push(NumCast(doc.y));
-        });
-        this.childDocs.map(doc => doc.transition = "transform 1s");
-        this.props.Document.timecode = Math.max(0, timecode + 1);
-        setTimeout(() => this.childDocs.map(doc => doc.transition = undefined), 1010);
-    }
-    @undoBatch
-    @action
-    backupInterpolated = (): void => {
-        this.childDocs.map(doc => doc.transition = "transform 1s");
-        this.props.Document.timecode = Math.max(0, NumCast(this.props.Document.timecode) - 1);
-        setTimeout(() => this.childDocs.map(doc => doc.transition = undefined), 1010);
-    }
-
-
     private thumbIdentifier?: number;
 
     onContextMenu = (e: React.MouseEvent) => {
@@ -1185,8 +1175,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
                 this._timelineVisible = !this._timelineVisible;
             }), icon: this._timelineVisible ? faEyeSlash : faEye
         });
-        ContextMenu.Instance.addItem({ description: "Advance", event: this.snaphsotInterpolated, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
-        ContextMenu.Instance.addItem({ description: "Backup ", event: this.backupInterpolated, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+        ContextMenu.Instance.addItem({ description: "Advance", event: this.nextKeyframe, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+        ContextMenu.Instance.addItem({ description: "Backup ", event: this.prevKeyframe, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
 
         const options = ContextMenu.Instance.findByDescription("Options...");
         const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
@@ -1342,18 +1332,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     render() {
         TraceMobx();
         const clientRect = this._mainCont?.getBoundingClientRect();
-        // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
-        // this.Document.fitX = this.contentBounds && this.contentBounds.x;
-        // this.Document.fitY = this.contentBounds && this.contentBounds.y;
-        // this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
-        // this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
-        // if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
-        // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
-        return <div className={"collectionfreeformview-container"}
-            ref={this.createDashEventsTarget}
+        return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget}
             onPointerOver={this.onPointerOver}
-            onWheel={this.onPointerWheel} onClick={this.onClick}  //pointerEvents: DraggingManager.GetIsDragging() ? "all" : undefined,
-            onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu}
+            onWheel={this.onPointerWheel}
+            onClick={this.onClick}
+            onPointerDown={this.onPointerDown}
+            onPointerMove={this.onCursorMove}
+            onDrop={this.onExternalDrop.bind(this)}
+            onContextMenu={this.onContextMenu}
             style={{
                 pointerEvents: this.backgroundEvents ? "all" : undefined,
                 transform: this.contentScaling ? `scale(${this.contentScaling})` : "",
@@ -1364,12 +1350,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
                 this.placeholder : this.marqueeView}
             <CollectionFreeFormOverlayView elements={this.elementFunc} />
-
+            {this.isAnnotationOverlay ? (null) :
+                <>
+                    <div key="back" style={{ position: "absolute", width: 20, height: 20, right: 0, bottom: 0, }} onClick={this.nextKeyframe}>
+                        <FontAwesomeIcon icon={"caret-right"} size={"lg"} />
+                    </div>
+                    <div key="fwd" style={{ position: "absolute", width: 20, height: 20, right: 20, bottom: 0, }}>
+                        {NumCast(this.props.Document.timecode)}
+                    </div>
+                    <div key="fwd" style={{ position: "absolute", width: 20, height: 20, right: 40, bottom: 0, }} onClick={this.prevKeyframe}>
+                        <FontAwesomeIcon icon={"caret-left"} size={"lg"} />
+                    </div>
+                </>}
             <div className={"pullpane-indicator"}
                 style={{
                     display: this._pullDirection ? "block" : "none",
                     top: clientRect ? this._pullDirection === "bottom" ? this._pullCoords[1] - clientRect.y : 0 : "auto",
-                    // left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x - MainView.Instance.flyoutWidth : 0 : "auto",
                     left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x : 0 : "auto",
                     width: clientRect ? this._pullDirection === "left" ? this._pullCoords[0] - clientRect.left : this._pullDirection === "right" ? clientRect.right - this._pullCoords[0] : clientRect.width : 0,
                     height: clientRect ? this._pullDirection === "top" ? this._pullCoords[1] - clientRect.top : this._pullDirection === "bottom" ? clientRect.bottom - this._pullCoords[1] : clientRect.height : 0,
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 5d6e587d9..710b2ce07 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -10,9 +10,14 @@ import React = require("react");
 import { Document } from "../../../fields/documentSchemas";
 import { TraceMobx } from "../../../fields/util";
 import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import { List } from "../../../fields/List";
+import { numberRange } from "../../../Utils";
+import { ComputedField } from "../../../fields/ScriptField";
+import { listSpec } from "../../../fields/Schema";
+import { docs } from "googleapis/build/src/apis/docs";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, highlight?: boolean, z: number, transition?: string } | undefined;
+    dataProvider?: (doc: Doc, replica: string) => { x: number, y: number, zIndex?: number, opacity?: number, highlight?: boolean, z: number, transition?: string } | undefined;
     sizeProvider?: (doc: Doc, replica: string) => { width: number, height: number } | undefined;
     zIndex?: number;
     highlight?: boolean;
@@ -35,6 +40,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${this.random(-1, 1) * this.props.jitterRotation}deg)`; }
     get X() { return this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
     get Y() { return this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+    get Opacity() { return this.dataProvider ? this.dataProvider.opacity : (this.Document.opacity || 0); }
     get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); }
     get Highlight() { return this.dataProvider?.highlight; }
     get width() { return this.props.sizeProvider && this.sizeProvider ? this.sizeProvider.width : this.layoutDoc[WidthSym](); }
@@ -62,6 +68,53 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         }
         return undefined;
     }
+
+    public static getValues(doc: Doc, time: number) {
+        return ({
+            x: Cast(doc["x-indexed"], listSpec("number"), []).reduce((p, x, i) => (i <= time && x !== undefined) || p === undefined ? x : p, undefined as any as number),
+            y: Cast(doc["y-indexed"], listSpec("number"), []).reduce((p, y, i) => (i <= time && y !== undefined) || p === undefined ? y : p, undefined as any as number),
+            opacity: Cast(doc["opacity-indexed"], listSpec("number"), []).reduce((p, o, i) => i <= time || p === undefined ? o : p, undefined as any as number),
+        });
+    }
+
+    public static setValues(timecode: number, d: Doc, x?: number, y?: number, opacity?: number) {
+        Cast(d["x-indexed"], listSpec("number"), [])[timecode] = x as any as number;
+        Cast(d["y-indexed"], listSpec("number"), null)[timecode] = y as any as number;
+        Cast(d["opacity-indexed"], listSpec("number"), null)[timecode] = opacity as any as number;
+    }
+    public static updateKeyframe(docs: Doc[], timecode: number) {
+        docs.forEach(doc => {
+            const xindexed = Cast(doc['x-indexed'], listSpec("number"), null);
+            const yindexed = Cast(doc['y-indexed'], listSpec("number"), null);
+            const opacityindexed = Cast(doc['opacity-indexed'], listSpec("number"), null);
+            xindexed.length <= timecode + 1 && xindexed.push(undefined as any as number);
+            yindexed.length <= timecode + 1 && yindexed.push(undefined as any as number);
+            opacityindexed.length <= timecode + 1 && opacityindexed.push(undefined as any as number);
+            doc.transition = "all 1s";
+        });
+        setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+    }
+
+    public static gotoKeyframe(docs: Doc[]) {
+        docs.forEach(doc => doc.transition = "all 1s");
+        setTimeout(() => docs.forEach(doc => doc.transition = undefined), 1010);
+    }
+
+    public static setupKeyframes(docs: Doc[], timecode: number, collection: Doc) {
+        docs.forEach(doc => {
+            doc["x-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]);
+            doc["y-indexed"] = new List<number>(numberRange(timecode).map(i => undefined) as any as number[]);
+            doc["opacity-indexed"] = new List<number>(numberRange(timecode).map(i => 0));
+            (doc["x-indexed"] as any).push(NumCast(doc.x));
+            (doc["y-indexed"] as any).push(NumCast(doc.y));
+            (doc["opacity-indexed"] as any).push(NumCast(doc.opacity, 1));
+            doc.timecode = ComputedField.MakeFunction("collection.timecode", {}, { collection });
+            doc.x = ComputedField.MakeInterpolated("x", "timecode");
+            doc.y = ComputedField.MakeInterpolated("y", "timecode");
+            doc.opacity = ComputedField.MakeInterpolated("opacity", "timecode");
+        });
+    }
+
     nudge = (x: number, y: number) => {
         this.props.Document.x = NumCast(this.props.Document.x) + x;
         this.props.Document.y = NumCast(this.props.Document.y) + y;
@@ -79,7 +132,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return <div className="collectionFreeFormDocumentView-container"
             style={{
                 boxShadow:
-                    this.layoutDoc.opacity === 0 ? undefined :  // if it's not visible, then no shadow
+                    this.Opacity === 0 ? undefined :  // if it's not visible, then no shadow
                         this.layoutDoc.z ? `#9c9396  ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` :  // if it's a floating doc, give it a big shadow
                             this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) :  // if it's just in a cluster, make the shadown roughly match the cluster border extent
                                 this.layoutDoc.isBackground ? undefined :  // if it's a background & has a cluster color, make the shadow spread really big
@@ -90,6 +143,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
                 width: this.width,
                 height: this.height,
+                opacity: this.Opacity,
                 zIndex: this.ZInd,
                 display: this.ZInd === -99 ? "none" : undefined,
                 pointerEvents: this.props.Document.isBackground ? "none" : this.props.pointerEvents ? "all" : undefined
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index f05f431ac..0ac1ac360 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -1,5 +1,5 @@
 import { ObjectField } from "./ObjectField";
-import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileError, CompileResult } from "../client/util/Scripting";
+import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions, CompileError, CompileResult, Scripting } from "../client/util/Scripting";
 import { Copy, ToScriptString, ToString, Parent, SelfProxy } from "./FieldSymbols";
 import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
 import { Deserializable, autoObject } from "../client/util/SerializationHelper";
@@ -156,12 +156,16 @@ export class ComputedField extends ScriptField {
         return compiled.compiled ? new ComputedField(compiled, setCompiled?.compiled ? setCompiled : undefined) : undefined;
     }
     public static MakeInterpolated(fieldKey: string, interpolatorKey: string) {
-        const getField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}]`, {}, true, {});
+        const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
         const setField = ScriptField.CompileScript(`(self['${fieldKey}-indexed'])[self.${interpolatorKey}] = value`, { value: "any" }, true, {});
         return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
     }
 }
 
+Scripting.addGlobal(function getIndexVal(list: any[], index: number) {
+    return list.reduce((p, x, i) => (i <= index && x !== undefined) || p === undefined ? x : p, undefined as any)
+});
+
 export namespace ComputedField {
     let useComputed = true;
     export function DisableComputedFields() {
-- 
cgit v1.2.3-70-g09d2

