import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { InkField } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { numberRange } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps } from './DocumentView'; import React = require('react'); export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined; renderCutoffProvider: (doc: Doc) => boolean; zIndex?: number; highlight?: boolean; jitterRotation: number; dataTransition?: string; replica: string; CollectionFreeFormView: CollectionFreeFormView; } @observer export class CollectionFreeFormDocumentView extends DocComponent() { public static animFields: { key: string; val?: number }[] = [ { key: '_height' }, { key: '_width' }, { key: 'x' }, { key: 'y' }, { key: '_jitterRotation', val: 0 }, { key: '_scrollTop' }, { key: 'opacity', val: 1 }, { key: 'viewScale', val: 1 }, { key: 'panX' }, { key: 'panY' }, ]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames @observable _animPos: number[] | undefined = undefined; @observable _contentView: DocumentView | undefined | null; get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')'; } get transform() { return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.jitterRotation, this.props.jitterRotation)}deg)`; } get X() { return this.dataProvider?.x ?? NumCast(this.Document.x); } get Y() { return this.dataProvider?.y ?? NumCast(this.Document.y); } get ZInd() { return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex); } get Opacity() { return this.dataProvider?.opacity; } get BackgroundColor() { return this.dataProvider?.backgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); } get Color() { return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null); } get Highlight() { return this.dataProvider?.highlight; } @computed get dataProvider() { return this.props.dataProvider?.(this.props.Document, this.props.replica); } @computed get sizeProvider() { return this.props.sizeProvider?.(this.props.Document, this.props.replica); } styleProvider = (doc: Doc | undefined, props: Opt, property: string) => { if (doc === this.layoutDoc) // prettier-ignore switch (property) { case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.BackgroundColor; case StyleProp.Color: return this.Color; } return this.props.styleProvider?.(doc, props, property); }; public static getValues(doc: Doc, time: number, fillIn: boolean = true) { return CollectionFreeFormDocumentView.animFields.reduce((p, val) => { p[val.key] = Cast(doc[`${val.key}-indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); return p; }, {} as { [val: string]: Opt }); } public static getStringValues(doc: Doc, time: number) { return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { p[val] = Cast(doc[`${val}-indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); return p; }, {} as { [val: string]: Opt }); } public static setValues(time: number, d: Doc, vals: { [val: string]: Opt }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice(); findexed[timecode] = vals[val] as any as number; d[`${val}-indexed`] = new List(findexed); }); } public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) { const timecode = Math.round(time); docs.forEach( action(doc => { doc._viewTransition = doc.dataTransition = 'all 1s'; CollectionFreeFormDocumentView.animFields.forEach(val => { const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); }); CollectionFreeFormDocumentView.animStringFields.forEach(val => { const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); }); CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any); }); }) ); setTimeout( () => docs.forEach(doc => { doc._viewTransition = undefined; doc.dataTransition = 'inherit'; }), 1010 ); } public static gotoKeyframe(docs: Doc[], duration = 1000) { docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`)); setTimeout( () => docs.forEach(doc => { doc._viewTransition = undefined; doc.dataTransition = 'inherit'; }), 1010 ); } public static setupZoom(doc: Doc, targDoc: Doc) { const width = new List(); const height = new List(); const top = new List(); const left = new List(); width.push(NumCast(targDoc._width)); height.push(NumCast(targDoc._height)); top.push(NumCast(targDoc._height) / -2); left.push(NumCast(targDoc._width) / -2); doc['viewfinder-width-indexed'] = width; doc['viewfinder-height-indexed'] = height; doc['viewfinder-top-indexed'] = top; doc['viewfinder-left-indexed'] = left; } public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { docs.forEach(doc => { if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; if (!doc['opacity-indexed']) { // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in doc['opacity-indexed'] = new List(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode))); const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); } @action public float = () => { const { Document: topDoc, ContainingCollectionView: container } = this.props; const screenXf = container?.screenToLocalTransform(); if (screenXf) { SelectionManager.DeselectAll(); if (topDoc.z) { const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y)); topDoc.z = 0; topDoc.x = spt[0]; topDoc.y = spt[1]; this.props.removeDocument?.(topDoc); this.props.addDocTab(topDoc, 'inParent'); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); topDoc.z = 1; topDoc.x = fpt[0]; topDoc.y = fpt[1]; } setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0); } }; 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; }; panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.(); panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.(); screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y); focusDoc = (doc: Doc) => this.props.focus(doc); returnThis = () => this; render() { TraceMobx(); const divProps: DocumentViewProps = { ...this.props, CollectionFreeFormDocumentView: this.returnThis, styleProvider: this.styleProvider, ScreenToLocalTransform: this.screenToLocalTransform, PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, }; return (
{this.props.renderCutoffProvider(this.props.Document) ? (
) : ( (this._contentView = r))} /> )}
); } }