aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/trails
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/trails')
-rw-r--r--src/client/views/nodes/trails/CubicBezierEditor.tsx202
-rw-r--r--src/client/views/nodes/trails/PresBox.scss170
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx1067
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx15
-rw-r--r--src/client/views/nodes/trails/PresEnums.ts2
-rw-r--r--src/client/views/nodes/trails/SlideEffect.scss19
-rw-r--r--src/client/views/nodes/trails/SlideEffect.tsx120
-rw-r--r--src/client/views/nodes/trails/SpringUtils.ts177
8 files changed, 1429 insertions, 343 deletions
diff --git a/src/client/views/nodes/trails/CubicBezierEditor.tsx b/src/client/views/nodes/trails/CubicBezierEditor.tsx
new file mode 100644
index 000000000..e1ad1e6e5
--- /dev/null
+++ b/src/client/views/nodes/trails/CubicBezierEditor.tsx
@@ -0,0 +1,202 @@
+import React, { useEffect, useState } from 'react';
+
+type Props = {
+ setFunc: (newPoints: { p1: number[]; p2: number[] }) => void;
+ currPoints: { p1: number[]; p2: number[] };
+};
+
+const ANIMATION_DURATION = 750;
+
+const CONTAINER_WIDTH = 200;
+const EDITOR_WIDTH = 100;
+const OFFSET = (CONTAINER_WIDTH - EDITOR_WIDTH) / 2;
+
+export const TIMING_DEFAULT_MAPPINGS = {
+ ease: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
+ linear: 'cubic-bezier(0.0, 0.0, 1.0, 1.0)',
+ 'ease-in': 'cubic-bezier(0.42, 0, 1.0, 1.0)',
+ 'ease-out': 'cubic-bezier(0, 0, 0.58, 1.0)',
+ 'ease-in-out': 'cubic-bezier(0.42, 0, 0.58, 1.0)',
+};
+
+export function EaseFuncToPoints(func: string) {
+ let strPoints = func || 'ease';
+ if (!strPoints.startsWith('cubic')) {
+ switch (func) {
+ case 'linear':
+ strPoints = 'cubic-bezier(0.0, 0.0, 1.0, 1.0)';
+ break;
+ case 'ease':
+ strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)';
+ break;
+ case 'ease-in':
+ strPoints = 'cubic-bezier(0.42, 0, 1.0, 1.0)';
+ break;
+ case 'ease-out':
+ strPoints = 'cubic-bezier(0, 0, 0.58, 1.0)';
+ break;
+ case 'ease-in-out':
+ strPoints = 'cubic-bezier(0.42, 0, 0.58, 1.0)';
+ break;
+ default:
+ strPoints = 'cubic-bezier(0.25, 0.1, 0.25, 1.0)';
+ }
+ }
+ const components = strPoints
+ .split('(')[1]
+ .split(')')[0]
+ .split(',')
+ .map(elem => parseFloat(elem));
+ return {
+ p1: [components[0], components[1]],
+ p2: [components[2], components[3]],
+ };
+}
+
+/**
+ * Visual editor for a bezier curve with draggable control points.
+ * */
+
+function CubicBezierEditor({ setFunc, currPoints }: Props) {
+ const [animating, setAnimating] = useState(false);
+ const [c1Down, setC1Down] = useState(false);
+ const [c2Down, setC2Down] = useState(false);
+
+ const roundToHundredth = (num: number) => Math.round(num * 100) / 100;
+
+ useEffect(() => {
+ if (animating) {
+ setTimeout(() => {
+ setAnimating(false);
+ }, ANIMATION_DURATION * 2);
+ }
+ }, [animating]);
+
+ useEffect(() => {
+ if (!c1Down) return undefined;
+ window.addEventListener('pointerup', () => {
+ setC1Down(false);
+ });
+ const handlePointerMove = (e: PointerEvent) => {
+ const newX = currPoints.p1[0] + e.movementX / EDITOR_WIDTH;
+ if (newX < 0 || newX > 1) {
+ return;
+ }
+
+ setFunc({
+ ...currPoints,
+ p1: [roundToHundredth(currPoints.p1[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p1[1] - e.movementY / EDITOR_WIDTH)],
+ });
+ };
+
+ window.addEventListener('pointermove', handlePointerMove);
+
+ return () => window.removeEventListener('pointermove', handlePointerMove);
+ }, [c1Down, currPoints]);
+
+ // Sets up pointer events for moving the control points
+ useEffect(() => {
+ if (!c2Down) return undefined;
+ window.addEventListener('pointerup', () => {
+ setC2Down(false);
+ });
+ const handlePointerMove = (e: PointerEvent) => {
+ const newX = currPoints.p2[0] + e.movementX / EDITOR_WIDTH;
+ if (newX < 0 || newX > 1) {
+ return;
+ }
+
+ setFunc({
+ ...currPoints,
+ p2: [roundToHundredth(currPoints.p2[0] + e.movementX / EDITOR_WIDTH), roundToHundredth(currPoints.p2[1] - e.movementY / EDITOR_WIDTH)],
+ });
+ };
+
+ window.addEventListener('pointermove', handlePointerMove);
+
+ return () => window.removeEventListener('pointermove', handlePointerMove);
+ }, [c2Down, currPoints]);
+
+ return (
+ <div
+ onPointerMove={e => {
+ e.stopPropagation;
+ }}>
+ <svg className="presBox-bezier-editor" width={`${CONTAINER_WIDTH}`} height={`${CONTAINER_WIDTH}`} xmlns="http://www.w3.org/2000/svg">
+ {/* Outlines */}
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${EDITOR_WIDTH + OFFSET}`} y2={`${0 + OFFSET}`} stroke="#c1c1c1" strokeWidth="1" />
+ {/* Box Outline */}
+ <rect x={`${0 + OFFSET}`} y={`${0 + OFFSET}`} width={EDITOR_WIDTH} height={EDITOR_WIDTH} stroke="#c5c5c5" fill="transparent" strokeWidth="1" />
+ {/* Editor */}
+ <path
+ d={`M ${0 + OFFSET} ${EDITOR_WIDTH + OFFSET} C ${currPoints.p1[0] * EDITOR_WIDTH + OFFSET} ${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}, ${
+ currPoints.p2[0] * EDITOR_WIDTH + OFFSET
+ } ${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}, ${EDITOR_WIDTH + OFFSET} ${0 + OFFSET}`}
+ stroke="#ffffff"
+ fill="transparent"
+ />
+ {/* Bottom left */}
+ <line
+ onPointerDown={() => {
+ setC1Down(true);
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ x1={`${0 + OFFSET}`}
+ y1={`${EDITOR_WIDTH + OFFSET}`}
+ x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${0 + OFFSET}`} y1={`${EDITOR_WIDTH + OFFSET}`} x2={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p1[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p1[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c1Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC1Down(true);
+ }}
+ onPointerUp={() => {
+ setC1Down(false);
+ }}
+ />
+ {/* Top right */}
+ <line
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ x1={`${EDITOR_WIDTH + OFFSET}`}
+ y1={`${0 + OFFSET}`}
+ x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ stroke="#00000000"
+ strokeWidth="5"
+ />
+ <line x1={`${EDITOR_WIDTH + OFFSET}`} y1={`${0 + OFFSET}`} x2={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`} y2={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`} stroke="#ffffff" strokeWidth="1" />
+ <circle
+ cx={`${currPoints.p2[0] * EDITOR_WIDTH + OFFSET}`}
+ cy={`${EDITOR_WIDTH - currPoints.p2[1] * EDITOR_WIDTH + OFFSET}`}
+ r="5"
+ fill={`${c2Down ? '#3fa9ff' : '#ffffff'}`}
+ onPointerDown={e => {
+ e.stopPropagation();
+ setC2Down(true);
+ }}
+ onPointerUp={() => {
+ setC2Down(false);
+ }}
+ />
+ </svg>
+ </div>
+ );
+}
+
+export default CubicBezierEditor;
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index 3b34a1f90..60d4e580d 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -1,5 +1,101 @@
@import '../../global/globalCssVariables.module.scss';
+.presBox-gpt-chat {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.pres-chat {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.presBox-icon-list {
+ display: flex;
+ gap: 8px;
+}
+
+.pres-chatbox-container {
+ padding: 16px;
+ outline: 1px solid #999999;
+ border-radius: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.pres-chatbox {
+ outline: none;
+ border: none;
+ resize: none;
+ font-family: Verdana, Geneva, sans-serif;
+ background-color: transparent;
+ overflow-y: hidden;
+}
+
+// Effect Animations
+
+.presBox-effects {
+ display: grid;
+ grid-template-columns: auto auto;
+ gap: 8px;
+}
+
+.presBox-effect-row {
+ display: flex;
+ gap: 8px;
+ margin: 4px;
+}
+
+.presBox-effect-container {
+ cursor: pointer;
+ overflow: hidden;
+ width: 80px;
+ height: 80px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ border: 1px solid rgb(118, 118, 118);
+ border-radius: 8px;
+}
+
+.presBox-effect-demo-box {
+ width: 40px;
+ height: 40px;
+ border-radius: 4px;
+ // default bg
+ background-color: rgb(37, 161, 255);
+}
+
+// Bezier editor
+
+.presBox-show-hide-dropdown {
+ cursor: pointer;
+ padding: 8px 0;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.presBox-bezier-editor {
+ border: 1px solid rgb(221, 221, 221);
+ border-radius: 4px;
+}
+
+.presBox-option-block {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 16px;
+}
+
+.presBox-option-center {
+ align-items: center;
+}
+
.presBox-cont {
cursor: auto;
position: absolute;
@@ -15,6 +111,29 @@
//overflow: hidden;
transition: 0.7s opacity ease;
+ .presBox-chatbox {
+ position: fixed;
+ bottom: 8px;
+ left: 8px;
+ width: calc(100% - 16px);
+ min-height: 100px;
+ border-radius: 16px;
+ padding: 16px;
+ gap: 8px;
+ z-index: 999;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+
+ .pres-chatbox {
+ outline: none;
+ border: none;
+ resize: none;
+ }
+ }
+
.presBox-listCont {
position: relative;
height: calc(100% - 67px);
@@ -150,6 +269,11 @@
}
}
+.presBox-toggles {
+ display: flex;
+ overflow-x: auto;
+}
+
.presBox-ribbon {
position: relative;
display: inline;
@@ -158,7 +282,9 @@
transition: 0.7s;
.ribbon-doubleButton {
- display: inline-flex;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.presBox-reactiveGrid {
@@ -186,16 +312,18 @@
.ribbon-property {
font-size: 11;
font-weight: 200;
- height: 20;
- display: flex;
- margin-left: 5px;
- margin-top: 5px;
- margin-bottom: 5px;
- width: max-content;
- justify-content: center;
- align-items: center;
- padding-right: 10px;
- padding-left: 10px;
+ padding: 8px;
+ border-radius: 4px;
+ // height: 20;
+ // display: flex;
+ // margin-left: 5px;
+ // margin-top: 5px;
+ // margin-bottom: 5px;
+ // width: max-content;
+ // justify-content: center;
+ // align-items: center;
+ // padding-right: 10px;
+ // padding-left: 10px;
}
.ribbon-propertyUpDown {
@@ -392,11 +520,16 @@
}
.presBox-input {
- width: 30;
- height: 100%;
- background: none;
border: none;
- text-align: right;
+ background-color: transparent;
+ width: 40;
+ // padding: 8px;
+ // border-radius: 4px;
+ // width: 30;
+ // height: 100%;
+ // background: none;
+ // border: none;
+ // text-align: right;
}
.presBox-input:focus {
@@ -606,15 +739,14 @@
background-color: $white;
display: flex;
color: $black;
- margin-top: 5px;
- margin-bottom: 5px;
border-radius: 5px;
- margin-right: 5px;
width: max-content;
justify-content: center;
align-items: center;
padding-right: 10px;
padding-left: 10px;
+ margin: 4px;
+ text-wrap: nowrap;
}
.ribbon-toggle.active {
@@ -638,7 +770,7 @@
grid-template-rows: max-content auto;
justify-self: center;
margin-top: 10px;
- padding-right: 10px;
+ // padding-right: 10px;
letter-spacing: normal;
width: 100%;
height: max-content;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 75492d2f9..0c73400a9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -2,9 +2,16 @@
/* eslint-disable jsx-a11y/click-events-have-key-events */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
+import Slider from '@mui/material/Slider';
+import { Button, Dropdown, DropdownType, IconButton, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import { BiMicrophone } from 'react-icons/bi';
+import { FaArrowDown, FaArrowLeft, FaArrowRight, FaArrowUp } from 'react-icons/fa';
+import ReactLoading from 'react-loading';
+import ReactTextareaAutosize from 'react-textarea-autosize';
import { lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../ClientUtils';
import { Doc, DocListCast, Field, FieldResult, FieldType, NumListCast, Opt, StrListCast } from '../../../../fields/Doc';
import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols';
@@ -16,9 +23,11 @@ import { listSpec } from '../../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast, toList } from '../../../../fields/Types';
import { emptyFunction, emptyPath, stringHash } from '../../../../Utils';
+import { getSlideTransitionSuggestions, gptSlideProperties, gptTrailSlideCustomization } from '../../../apis/gpt/PresCustomization';
import { DocServer } from '../../../DocServer';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { DictationManager } from '../../../util/DictationManager';
import { dropActionType } from '../../../util/DropActionTypes';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SerializationHelper } from '../../../util/SerializationHelper';
@@ -36,8 +45,11 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { FocusViewOptions } from '../FocusViewOptions';
import { OpenWhere, OpenWhereMod } from '../OpenWhere';
import { ScriptingBox } from '../ScriptingBox';
+import CubicBezierEditor, { EaseFuncToPoints, TIMING_DEFAULT_MAPPINGS } from './CubicBezierEditor';
import './PresBox.scss';
import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import SlideEffect from './SlideEffect';
+import { AnimationSettings, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors, SpringSettings, SpringType } from './SpringUtils';
@observer
export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -85,7 +97,100 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _treeViewMap: Map<Doc, number> = new Map();
@observable _presKeyEvents: boolean = false;
@observable _forceKeyEvents: boolean = false;
- @computed get isTreeOrStack() {
+
+ // GPT
+ private _inputref: HTMLTextAreaElement | null = null;
+ private _inputref2: HTMLTextAreaElement | null = null;
+ @observable chatActive: boolean = false;
+ @observable chatInput: string = '';
+ public slideToModify: Doc | null = null;
+ @observable isRecording: boolean = false;
+ @observable isLoading: boolean = false;
+
+ @observable generatedAnimations: AnimationSettings[] = [
+ // default presets
+ {
+ effect: PresEffect.Bounce,
+ direction: PresEffectDirection.Left,
+ stiffness: 400,
+ damping: 15,
+ mass: 1,
+ },
+ {
+ effect: PresEffect.Fade,
+ direction: PresEffectDirection.Left,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ {
+ effect: PresEffect.Flip,
+ direction: PresEffectDirection.Left,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ {
+ effect: PresEffect.Rotate,
+ direction: PresEffectDirection.Left,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ ];
+
+ @action
+ setGeneratedAnimations = (settings: AnimationSettings[]) => {
+ this.generatedAnimations = settings;
+ };
+
+ @observable animationChat: string = '';
+
+ @action
+ setChatInput = (input: string) => {
+ this.chatInput = input;
+ };
+
+ @action
+ setAnimationChat = (input: string) => {
+ this.animationChat = input;
+ };
+
+ @action
+ setIsLoading = (isLoading: boolean) => {
+ this.isLoading = isLoading;
+ };
+
+ @action
+ public setIsRecording = (isRecording: boolean) => {
+ this.isRecording = isRecording;
+ };
+
+ @observable showBezierEditor = false;
+ @action setBezierEditorVisibility = (visible: boolean) => {
+ this.showBezierEditor = visible;
+ };
+ @observable showSpringEditor = true;
+ @action setSpringEditorVisibility = (visible: boolean) => {
+ this.showSpringEditor = visible;
+ };
+
+ // Easing function variables
+
+ @observable easeDropdownVal = 'ease';
+
+ @action setBezierControlPoints = (newPoints: { p1: number[]; p2: number[] }) => {
+ this.setEaseFunc(this.activeItem, `cubic-bezier(${newPoints.p1[0]}, ${newPoints.p1[1]}, ${newPoints.p2[0]}, ${newPoints.p2[1]})`);
+ };
+
+ @computed
+ get currCPoints() {
+ const strPoints = this.activeItem.presentation_easeFunc ? StrCast(this.activeItem.presentation_easeFunc) : 'ease';
+ return EaseFuncToPoints(strPoints);
+ }
+
+ @computed
+ get isTreeOrStack() {
return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any);
}
@computed get isTree() {
@@ -213,6 +318,85 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
+ // Recording for GPT customization
+
+ recordDictation = () => {
+ this.setIsRecording(true);
+ this.setChatInput('');
+ DictationManager.Controls.listen({
+ interimHandler: this.setDictationContent,
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ });
+ };
+ stopDictation = () => {
+ this.setIsRecording(false);
+ DictationManager.Controls.stop();
+ };
+
+ setDictationContent = (value: string) => {
+ console.log('Dictation value', value);
+ this.setChatInput(value);
+ };
+
+ @action
+ customizeAnimations = async () => {
+ this.setIsLoading(true);
+ try {
+ const res = await getSlideTransitionSuggestions(this.animationChat);
+ if (typeof res === 'string') {
+ const resObj = JSON.parse(res);
+ console.log('Parsed GPT Result ', resObj);
+ this.setGeneratedAnimations(resObj as AnimationSettings[]);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ this.setIsLoading(false);
+ };
+
+ @action
+ customizeWithGPT = async (input: string) => {
+ // const testInput = 'change title to Customized Slide, transition for 2.3s with fade in effect';
+ this.setIsRecording(false);
+ this.setIsLoading(true);
+
+ const currSlideProperties: { [key: string]: any } = {};
+ gptSlideProperties.forEach(key => {
+ if (this.activeItem[key]) {
+ currSlideProperties[key] = this.activeItem[key];
+ }
+ // default values
+ else if (key === 'presentation_transition') {
+ currSlideProperties[key] = 500;
+ } else if (key === 'config_zoom') {
+ currSlideProperties[key] = 1.0;
+ }
+ });
+ console.log('current slide props ', currSlideProperties);
+
+ try {
+ const res = await gptTrailSlideCustomization(input, currSlideProperties);
+ if (typeof res === 'string') {
+ const resObj = JSON.parse(res);
+ console.log('Parsed GPT Result ', resObj);
+ // eslint-disable-next-line no-restricted-syntax
+ for (const key in resObj) {
+ if (resObj[key]) {
+ console.log('typeof property', typeof resObj[key]);
+ this.activeItem[key] = resObj[key];
+ }
+ }
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ this.setIsLoading(false);
+ };
+
// TODO: al: it seems currently that tempMedia doesn't stop onslidechange after clicking the button; the time the tempmedia stop depends on the start & end time
// TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
// No more frames in current doc and next slide is defined, therefore move to next slide
@@ -664,6 +848,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return;
}
const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined;
+ // default with effect: 750ms else 500ms
const presTime = NumCast(activeItem.presentation_transition, effect ? 750 : 500);
const options: FocusViewOptions = {
willPan: activeItem.presentation_movement !== PresMovement.None,
@@ -673,9 +858,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
effect: activeItem,
noSelect: true,
openLocation: targetDoc.type === DocumentType.PRES ? ((OpenWhere.replace + ':' + PresBox.PanelName) as OpenWhere) : OpenWhere.addLeft,
- easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
+ easeFunc: StrCast(activeItem.presentation_easeFunc, 'ease') as any,
zoomTextSelections: BoolCast(activeItem.presentation_zoomText),
- playAudio: BoolCast(activeItem.presPlayAudio),
+ playAudio: BoolCast(activeItem.presentation_playAudio),
playMedia: activeItem.presentation_mediaStart === 'auto',
};
if (activeItem.presentation_openInLightbox) {
@@ -1109,6 +1294,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
keyEvents = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement) return;
+ if (e.target instanceof HTMLTextAreaElement) return;
let handled = false;
const anchorNode = document.activeElement as HTMLDivElement;
if (anchorNode && anchorNode.className?.includes('lm_title')) return;
@@ -1389,9 +1575,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
updateEaseFunc = (activeItem: Doc) => {
- activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear';
+ activeItem.presentation_easeFunc = activeItem.presentation_easeFunc === 'linear' ? 'ease' : 'linear';
+ this.selectedArray.forEach(doc => {
+ doc.presentation_easeFunc = activeItem.presentation_easeFunc;
+ });
+ };
+
+ setEaseFunc = (activeItem: Doc, easeFunc: string) => {
+ activeItem.presentation_easeFunc = easeFunc;
this.selectedArray.forEach(doc => {
- doc.presEaseFunc = activeItem.presEaseFunc;
+ doc.presentation_easeFunc = activeItem.presentation_easeFunc;
});
};
@@ -1407,6 +1600,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect);
});
+ @undoBatch
+ updateEffectTiming = (activeItem: Doc, timing: SpringSettings) => {
+ activeItem.presentation_effectTiming = JSON.stringify(timing);
+ this.selectedArray.forEach(doc => {
+ doc.presentation_effectTiming = activeItem.presentation_effectTiming;
+ });
+ };
+
static _sliderBatch: any;
static endBatch = () => {
PresBox._sliderBatch.end();
@@ -1434,6 +1635,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
);
+ // Applies the slide transiiton settings to all docs in the array
@undoBatch
applyTo = (array: Doc[]) => {
this.updateMovement(this.activeItem.presentation_movement as PresMovement, true);
@@ -1457,79 +1659,68 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 1000 : 0;
if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
return (
- <div className="presBox-ribbon">
- <div className="ribbon-doubleButton">
- <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}>
- <div
- className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`}
- style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
- onClick={() => this.updateHideBefore(activeItem)}>
- Hide before
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">Hide while presented</div>}>
- <div
- className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`}
- style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
- onClick={() => this.updateHide(activeItem)}>
- Hide
- </div>
- </Tooltip>
-
- <Tooltip title={<div className="dash-tooltip">Hide after presented</div>}>
- <div
- className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`}
- style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
- onClick={() => this.updateHideAfter(activeItem)}>
- Hide after
- </div>
- </Tooltip>
-
- <Tooltip title={<div className="dash-tooltip">Open in lightbox view</div>}>
- <div
- className="ribbon-toggle"
- style={{
- border: `solid 1px ${SnappingManager.userColor}`,
- color: SnappingManager.userColor,
- background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
- }}
- onClick={() => this.updateOpenDoc(activeItem)}>
- Lightbox
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}>
- <div
- className="ribbon-toggle"
- style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
- onClick={() => this.updateEaseFunc(activeItem)}>
- {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
- </div>
- </Tooltip>
- </div>
- {[DocumentType.AUDIO, DocumentType.VID].find(dt => dt === targetType) ? null : (
- <>
- <div className="ribbon-doubleButton">
- <div className="presBox-subheading">Slide Duration</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+ <div className="presBox-option-block">
+ <div className="presBox-ribbon">
+ <div className="presBox-toggles">
+ <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`}
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: activeItem.presentation_hideBefore ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => this.updateHideBefore(activeItem)}>
+ Hide before
</div>
- <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
- <FontAwesomeIcon icon="caret-up" />
- </div>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}>
- <FontAwesomeIcon icon="caret-down" />
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Hide while presented</div>}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`}
+ style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hide ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
+ onClick={() => this.updateHide(activeItem)}>
+ Hide
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">Hide after presented</div>}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`}
+ style={{ border: `solid 1px ${SnappingManager.userColor}`, color: SnappingManager.userColor, background: activeItem.presentation_hideAfter ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor }}
+ onClick={() => this.updateHideAfter(activeItem)}>
+ Hide after
+ </div>
+ </Tooltip>
+
+ <Tooltip title={<div className="dash-tooltip">Open in lightbox view</div>}>
+ <div
+ className="ribbon-toggle"
+ style={{
+ border: `solid 1px ${SnappingManager.userColor}`,
+ color: SnappingManager.userColor,
+ background: activeItem.presentation_openInLightbox ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor,
+ }}
+ onClick={() => this.updateOpenDoc(activeItem)}>
+ Lightbox
+ </div>
+ </Tooltip>
+ </div>
+ {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : (
+ <>
+ <div className="ribbon-doubleButton">
+ <div className="presBox-subheading">Slide Duration</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+ <input className="presBox-input" type="number" readOnly value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
</div>
</div>
- </div>
- {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
- <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
- <div className="slider-text">Short</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Long</div>
- </div>
- </>
- )}
+ {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
+ <div className="slider-headers" style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
+ <div className="slider-text">Short</div>
+ <div className="slider-text">Medium</div>
+ <div className="slider-text">Long</div>
+ </div>
+ </>
+ )}
+ </div>
</div>
);
}
@@ -1548,78 +1739,80 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
);
return (
- <div className="presBox-ribbon">
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Progressivize Collection</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined;
- activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined;
- const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
- const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
- activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0;
- // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
- // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
- let dataField = Doc.LayoutFieldKey(tagDoc);
- if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations';
-
- if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`);
- else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`);
- }}
- checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Progressivize First Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1;
- }}
- checked={!NumCast(activeItem.presentation_indexedStart)}
- />
- </div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Expand Current Bullet</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presBulletExpand = !activeItem.presBulletExpand;
- }}
- checked={BoolCast(activeItem.presBulletExpand)}
- />
- </div>
+ <div className="presBox-option-block">
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize Collection</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
+ type="checkbox"
+ onChange={() => {
+ activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined;
+ activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined;
+ const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
+ const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
+ activeItem.presentation_indexedStart = type === DocumentType.COL ? 1 : 0;
+ // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
+ // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
+ let dataField = Doc.LayoutFieldKey(tagDoc);
+ if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField += '_annotations';
+
+ if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc.annotationOn?.["${dataField}"]`);
+ else activeItem.data = ComputedField.MakeFunction(`this.presentation_targetDoc?.["${dataField}"]`);
+ }}
+ checked={Cast(activeItem.presentation_indexed, 'number', null) !== undefined}
+ />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize First Bullet</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
+ type="checkbox"
+ onChange={() => {
+ activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1;
+ }}
+ checked={!NumCast(activeItem.presentation_indexedStart)}
+ />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Expand Current Bullet</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
+ type="checkbox"
+ onChange={() => {
+ activeItem.presBulletExpand = !activeItem.presBulletExpand;
+ }}
+ checked={BoolCast(activeItem.presBulletExpand)}
+ />
+ </div>
- <div className="ribbon-box">
- Bullet Effect
- <div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
- })}
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
- border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
- }}>
- {effect?.toString()}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
+ <div className="ribbon-box">
+ Bullet Effect
<div
- className="presBox-dropdownOptions"
- style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
- onPointerDown={e => e.stopPropagation()}>
- {Object.values(PresEffect)
- .filter(v => isNaN(Number(v)))
- .map(peffect => bulletEffect(peffect))}
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
+ })}
+ style={{
+ color: SnappingManager.userColor,
+ background: SnappingManager.userVariantColor,
+ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
+ border: this._openBulletEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
+ }}>
+ {effect?.toString()}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
+ <div
+ className="presBox-dropdownOptions"
+ style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
+ onPointerDown={e => e.stopPropagation()}>
+ {Object.values(PresEffect)
+ .filter(v => isNaN(Number(v)))
+ .map(pEffect => bulletEffect(pEffect))}
+ </div>
</div>
</div>
</div>
@@ -1628,190 +1821,428 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
return null;
}
+
+ @computed get gptDropdown() {
+ return <div />;
+ }
+
@computed get transitionDropdown() {
const { activeItem } = this;
- const preseEffect = (effect: PresEffect) => (
- <div
- className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
- onPointerDown={StopEvent}
- onClick={() => this.updateEffect(effect, false)}>
- {effect}
- </div>
- );
- const presMovement = (movement: PresMovement) => (
- <div className={`presBox-dropdownOption ${activeItem.presentation_movement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}>
- {movement}
- </div>
- );
- const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
- const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SnappingManager.userVariantColor : SnappingManager.userColor;
- return (
- <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
- <div
- style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
- onClick={() => this.updateEffectDirection(direction)}>
- {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
- </div>
- </Tooltip>
- );
- };
+ // Retrieving spring timing properties
+ const timing = StrCast(activeItem.presentation_effectTiming);
+ let timingConfig: SpringSettings | undefined;
+ if (timing) {
+ timingConfig = JSON.parse(timing);
+ }
+
+ if (!timingConfig) {
+ timingConfig = {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ };
+ }
+
if (activeItem && this.targetDoc) {
const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5;
const zoom = NumCast(activeItem.config_zoom, 1) * 100;
- const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None;
+ const effect = StrCast(activeItem.presentation_effect) ? (StrCast(activeItem.presentation_effect) as any as PresEffect) : PresEffect.None;
+ const direction = StrCast(activeItem.presentation_effectDirection) as PresEffectDirection;
+
return (
- <div
- className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
- onPointerDown={StopEvent}
- onPointerUp={StopEvent}
- onClick={action(e => {
- e.stopPropagation();
- this._openMovementDropdown = false;
- this._openEffectDropdown = false;
- this._openBulletEffectDropdown = false;
- })}>
- <div className="ribbon-box">
- Movement
- <div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openMovementDropdown = !this._openMovementDropdown;
- })}
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5,
- border: this._openMovementDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
- }}>
- {this.movementName(activeItem)}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
- <div
- className="presBox-dropdownOptions"
- id="presBoxMovementDropdown"
- onPointerDown={StopEvent}
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userBackgroundColor,
- display: this._openMovementDropdown ? 'grid' : 'none',
- }}>
- {presMovement(PresMovement.None)}
- {presMovement(PresMovement.Center)}
- {presMovement(PresMovement.Zoom)}
- {presMovement(PresMovement.Pan)}
- {presMovement(PresMovement.Jump)}
+ <>
+ {/* This chatbox is for customizing the properties of trails, like transition time, movement type (zoom, pan) using GPT */}
+ <div className="presBox-gpt-chat">
+ <span style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
+ Customize Slide Properties{' '}
+ <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/trails/#slide-customization')}>
+ <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SnappingManager.userColor} />
</div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Zoom (% screen filled)</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+ </span>
+ <div className="pres-chat">
+ <div className="pres-chatbox-container">
+ <ReactTextareaAutosize
+ placeholder="Describe how you would like to modify the slide properties."
+ className="pres-chatbox"
+ value={this.chatInput}
+ onChange={e => {
+ this.setChatInput(e.target.value);
+ }}
+ onKeyDown={e => {
+ this.stopDictation();
+ e.stopPropagation();
+ }}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={this.isRecording ? '#2bcaff' : SnappingManager.userVariantColor}
+ tooltip="Record"
+ icon={<BiMicrophone size="16px" />}
+ onClick={() => {
+ if (!this.isRecording) {
+ this.recordDictation();
+ } else {
+ this.stopDictation();
+ }
+ }}
+ />
</div>
- <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
- <FontAwesomeIcon icon="caret-up" />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ type={Type.TERT}
+ icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SnappingManager.userVariantColor}
+ onClick={() => {
+ this.stopDictation();
+ this.customizeWithGPT(this.chatInput);
+ }}
+ />
+ </div>
+ </div>
+ {/* Movement */}
+ <div
+ className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
+ onPointerDown={StopEvent}
+ onPointerUp={StopEvent}
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openMovementDropdown = false;
+ this._openEffectDropdown = false;
+ this._openBulletEffectDropdown = false;
+ })}>
+ <div
+ className="presBox-option-block"
+ // style={{ padding: '16px' }}
+ >
+ Movement
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Movement"
+ closeOnSelect
+ items={movementItems}
+ selectedVal={this.movementName(activeItem)}
+ setSelectedVal={val => {
+ this.updateMovement(val as PresMovement);
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
+ <div className="presBox-subheading">Zoom (% screen filled)</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+ <input className="presBox-input" readOnly type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
</div>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}>
- <FontAwesomeIcon icon="caret-down" />
+ </div>
+ {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Transition Time</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+ <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
</div>
</div>
- </div>
- {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Transition Time</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- <input className="presBox-input" type="number" readOnly value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
+ {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.updateTransitionTime)}
+ <div className="slider-headers">
+ <div className="slider-text">Fast</div>
+ <div className="slider-text">Medium</div>
+ <div className="slider-text">Slow</div>
</div>
- <div className="ribbon-propertyUpDown" style={{ color: SnappingManager.userBackgroundColor, background: SnappingManager.userColor }}>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
- <FontAwesomeIcon icon="caret-up" />
- </div>
- <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}>
- <FontAwesomeIcon icon="caret-down" />
- </div>
+ {/* Easing function */}
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Easing Function"
+ closeOnSelect
+ items={easeItems}
+ selectedVal={this.activeItem.presentation_easeFunc ? (StrCast(this.activeItem.presentation_easeFunc).startsWith('cubic') ? 'custom' : StrCast(this.activeItem.presentation_easeFunc)) : 'ease'}
+ setSelectedVal={val => {
+ if (typeof val === 'string') {
+ if (val !== 'custom') {
+ this.setEaseFunc(this.activeItem, val);
+ } else {
+ this.setBezierEditorVisibility(true);
+ this.setEaseFunc(this.activeItem, TIMING_DEFAULT_MAPPINGS.ease);
+ }
+ }
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+ {/* Custom */}
+ <div
+ className="presBox-show-hide-dropdown"
+ style={{ alignSelf: 'flex-start' }}
+ onClick={e => {
+ e.stopPropagation();
+ this.setBezierEditorVisibility(!this.showBezierEditor);
+ }}>
+ {`${this.showBezierEditor ? 'Hide' : 'Show'} Timing Editor`}
+ <FontAwesomeIcon icon={this.showBezierEditor ? 'chevron-up' : 'chevron-down'} />
</div>
</div>
- {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)}
- <div className="slider-headers">
- <div className="slider-text">Fast</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Slow</div>
- </div>
</div>
- <div className="ribbon-box">
+
+ {/* Cubic bezier editor */}
+ {this.showBezierEditor && (
+ <div className="presBox-option-block" style={{ paddingTop: 0 }}>
+ <p className="presBox-submenu-label" style={{ alignSelf: 'flex-start' }}>
+ Custom Timing Function
+ </p>
+ <CubicBezierEditor setFunc={this.setBezierControlPoints} currPoints={this.currCPoints} />
+ </div>
+ )}
+
+ {/* This chatbox is for getting slide effect transition suggestions from gpt and visualizing them */}
+ <div className="presBox-gpt-chat">
Effects
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Play Audio Annotation</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio);
- }}
- checked={BoolCast(activeItem.presPlayAudio)}
+ <div className="pres-chat">
+ <div className="pres-chatbox-container">
+ <ReactTextareaAutosize
+ placeholder="Customize prompt for effect suggestions. Leave blank for random results."
+ className="pres-chatbox"
+ value={this.animationChat}
+ onChange={e => {
+ this.setAnimationChat(e.target.value);
+ }}
+ onKeyDown={e => {
+ this.stopDictation();
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ type={Type.TERT}
+ icon={this.isLoading ? <ReactLoading type="spin" color="#ffffff" width={20} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SnappingManager.userVariantColor}
+ onClick={this.customizeAnimations}
/>
</div>
- <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
- <div className="presBox-subheading">Zoom Text Selections</div>
- <input
- className="presBox-checkbox"
- style={{ margin: 10, border: `solid 1px ${SnappingManager.userColor}` }}
- type="checkbox"
- onChange={() => {
- activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
+ </div>
+
+ <div
+ className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
+ onPointerDown={StopEvent}
+ onPointerUp={StopEvent}
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openMovementDropdown = false;
+ this._openEffectDropdown = false;
+ this._openBulletEffectDropdown = false;
+ })}>
+ <div className="presBox-option-block">
+ Click on a box to apply the effect.
+ <div className="presBox-option-block presBox-option-center">
+ {/* Preview Animations */}
+ <div className="presBox-effects">
+ {this.generatedAnimations.map((elem, i) => (
+ <div
+ // eslint-disable-next-line react/no-array-index-key
+ key={i}
+ className="presBox-effect-container"
+ onClick={() => {
+ this.updateEffect(elem.effect, false);
+ this.updateEffectDirection(elem.direction);
+ this.updateEffectTiming(this.activeItem, {
+ type: SpringType.CUSTOM,
+ stiffness: elem.stiffness,
+ damping: elem.damping,
+ mass: elem.mass,
+ });
+ }}>
+ <SlideEffect dir={elem.direction} presEffect={elem.effect} springSettings={elem} infinite>
+ <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[i] }} />
+ </SlideEffect>
+ </div>
+ ))}
+ </div>
+ </div>
+ {/* Effect dropdown */}
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Slide Effect"
+ closeOnSelect
+ items={effectItems}
+ selectedVal={effect?.toString()}
+ setSelectedVal={val => {
+ this.updateEffect(val as PresEffect, false);
+ // set default spring options for that effect
+ this.updateEffectTiming(activeItem, presEffectDefaultTimings[val as keyof typeof presEffectDefaultTimings]);
}}
- checked={BoolCast(activeItem.presentation_zoomText)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
/>
+ {/* Effect direction */}
+ {/* Only applies to certain effects */}
+ {(effect === PresEffect.Flip || effect === PresEffect.Bounce || effect === PresEffect.Roll) && (
+ <>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Effect direction</div>
+ <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
+ {StrCast(this.activeItem.presentation_effectDirection)}
+ </div>
+ </div>
+ <div className="presBox-icon-list">
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Left ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Left"
+ icon={<FaArrowRight size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Left)}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Right ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Right"
+ icon={<FaArrowLeft size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Right)}
+ />
+ {effect !== PresEffect.Roll && (
+ <>
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Top ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Top"
+ icon={<FaArrowDown size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Top)}
+ />
+ <IconButton
+ type={Type.TERT}
+ color={activeItem.presentation_effectDirection === PresEffectDirection.Bottom ? SnappingManager.userVariantColor : SnappingManager.userBackgroundColor}
+ tooltip="Bottom"
+ icon={<FaArrowUp size="16px" />}
+ onClick={() => this.updateEffectDirection(PresEffectDirection.Bottom)}
+ />
+ </>
+ )}
+ </div>
+ </>
+ )}
+ {/* Spring settings */}
+ {/* No spring settings for jackinthebox (lightspeed) */}
+ {effect !== PresEffect.Lightspeed && (
+ <>
+ <Dropdown
+ color={SnappingManager.userColor}
+ formLabel="Effect Timing"
+ closeOnSelect
+ items={effectTimings}
+ selectedVal={timingConfig.type}
+ setSelectedVal={val => {
+ this.updateEffectTiming(activeItem, {
+ type: val as SpringType,
+ ...springMappings[val],
+ });
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ />
+ <div
+ className="presBox-show-hide-dropdown"
+ onClick={e => {
+ e.stopPropagation();
+ this.setSpringEditorVisibility(!this.showSpringEditor);
+ }}>
+ {`${this.showSpringEditor ? 'Hide' : 'Show'} Spring Settings`}
+ <FontAwesomeIcon icon={this.showSpringEditor ? 'chevron-up' : 'chevron-down'} />
+ </div>
+ {this.showSpringEditor && (
+ <>
+ <div>Tension</div>
+ <div
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}>
+ <Slider
+ min={1}
+ max={1000}
+ step={5}
+ size="small"
+ value={timingConfig.stiffness}
+ onChange={(e, val) => {
+ if (!timingConfig) return;
+ this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, stiffness: val as number });
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div>Damping</div>
+ <div
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}>
+ <Slider
+ min={1}
+ max={100}
+ step={1}
+ size="small"
+ value={timingConfig.damping}
+ onChange={(e, val) => {
+ if (!timingConfig) return;
+ this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, damping: val as number });
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div>Mass</div>
+ <div
+ onPointerDown={e => {
+ e.stopPropagation();
+ }}>
+ <Slider
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={timingConfig.mass}
+ onChange={(e, val) => {
+ if (!timingConfig) return;
+ this.updateEffectTiming(activeItem, { ...timingConfig, type: SpringType.CUSTOM, mass: val as number });
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ Preview Effect
+ <div className="presBox-option-block presBox-option-center">
+ <div className="presBox-effect-container">
+ <SlideEffect dir={direction} presEffect={effect} springSettings={timingConfig} infinite>
+ <div className="presBox-effect-demo-box" style={{ backgroundColor: springPreviewColors[0] }} />
+ </SlideEffect>
+ </div>
+ </div>
+ </>
+ )}
+ </>
+ )}
</div>
- <div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openEffectDropdown = !this._openEffectDropdown;
- })}
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userVariantColor,
- borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5,
- border: this._openEffectDropdown ? `solid 2px ${SnappingManager.userVariantColor}` : `solid 1px ${SnappingManager.userColor}`,
- }}>
- {effect?.toString()}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon="angle-down" />
- <div
- className="presBox-dropdownOptions"
- id="presBoxMovementDropdown"
- style={{
- color: SnappingManager.userColor,
- background: SnappingManager.userBackgroundColor,
- display: this._openEffectDropdown ? 'grid' : 'none',
+
+ {/* Toggles */}
+ <div className="presBox-option-block">
+ <Toggle
+ formLabel="Play Audio Annotation"
+ toggleType={ToggleType.SWITCH}
+ toggleStatus={BoolCast(activeItem.presentation_playAudio)}
+ onClick={() => {
+ activeItem.presentation_playAudio = !BoolCast(activeItem.presentation_playAudio);
}}
- onPointerDown={e => e.stopPropagation()}>
- {Object.values(PresEffect)
- .filter(v => isNaN(Number(v)))
- .map(presEffect => preseEffect(presEffect))}
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
- <div className="presBox-subheading">Effect direction</div>
- <div className="ribbon-property" style={{ border: `solid 1px ${SnappingManager.userColor}` }}>
- {StrCast(this.activeItem.presentation_effectDirection)}
- </div>
- </div>
- <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}>
- {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
- {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
- {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
- {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
- {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
- </div>
- </div>
- <div className="ribbon-final-box">
- <div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}>
- Apply to all
+ color={SnappingManager.userColor}
+ />
+ <Toggle
+ formLabel="Zoom Text Selections"
+ toggleType={ToggleType.SWITCH}
+ toggleStatus={BoolCast(activeItem.presentation_zoomText)}
+ onClick={() => {
+ activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText);
+ }}
+ color={SnappingManager.userColor}
+ />
+ <Button text="Apply to all" type={Type.TERT} color={SnappingManager.userVariantColor} onClick={() => this.applyTo(this.childDocs)} />
</div>
</div>
- </div>
+ </>
);
}
return undefined;
@@ -1995,23 +2426,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
<div>On slide change</div>
</div>
- {/* <div className="checkbox-container">
- <input className="presBox-checkbox"
- type="checkbox"
- onChange={() => activeItem.mediaStop = "afterSlide"}
- checked={activeItem.mediaStop === "afterSlide"}
- />
- <div className="checkbox-dropdown">
- After chosen slide
- <select className="presBox-viewPicker"
- style={{ opacity: activeItem.mediaStop === "afterSlide" && this.itemIndex !== this.childDocs.length - 1 ? 1 : 0.3 }}
- onPointerDown={e => e.stopPropagation()}
- onChange={this.mediaStopChanged}
- value={mediaStopDocStr}>
- {this.mediaStopSlides}
- </select>
- </div>
- </div> */}
</div>
</div>
</div>
@@ -2284,6 +2698,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
SnappingManager.SetPropertiesWidth(SnappingManager.PropertiesWidth > 0 ? 0 : 250);
};
+ @action
+ openProperties = () => {
+ // need to also focus slide
+ SnappingManager.SetPropertiesWidth(250);
+ };
+
@computed get toolbar() {
const propIcon = SnappingManager.PropertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
const propTitle = SnappingManager.PropertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
@@ -2670,7 +3090,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
) : null}
</div>
-
{/* {
// if the document type is a presentation, then the collection stacking view has a "+ new slide" button at the bottom of the stack
<Tooltip title={<div className="dash-tooltip">{'Click on document to pin to presentaiton or make a marquee selection to pin your desired view'}</div>}>
@@ -2680,6 +3099,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
} */}
</div>
+ {/* presbox chatbox */}
+ {this.chatActive && <div className="presBox-chatbox" />}
</div>
);
}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 306b98190..25adfba23 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -197,6 +197,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
* Function to drag and drop the pres element to a diferent location
*/
startDrag = (e: PointerEvent) => {
+ this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false);
const miniView: boolean = this.toolbarWidth <= 100;
const activeItem = this.slideDoc;
const dragArray = this.presBoxView?._dragArray ?? [];
@@ -524,6 +525,20 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
);
+ items.push(
+ <Tooltip key="customize-slide" title={<div className="dash-tooltip">Customize Slide</div>}>
+ <div
+ className={'slideButton'}
+ onClick={() => {
+ this.presBoxView?.regularSelect(this.slideDoc, this._itemRef.current!, this._dragRef.current!, true, false);
+ PresBox.Instance.navigateToActiveItem();
+ PresBox.Instance.openProperties();
+ PresBox.Instance.slideToModify = this.Document;
+ }}>
+ <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ );
return items;
}
diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index 564829d54..67cad9c5d 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -7,7 +7,7 @@ export enum PresMovement {
}
export enum PresEffect {
- Zoom = 'Zoom',
+ Expand = 'Expand',
Lightspeed = 'Lightspeed',
Fade = 'Fade in',
Flip = 'Flip',
diff --git a/src/client/views/nodes/trails/SlideEffect.scss b/src/client/views/nodes/trails/SlideEffect.scss
new file mode 100644
index 000000000..aa2e5bbd9
--- /dev/null
+++ b/src/client/views/nodes/trails/SlideEffect.scss
@@ -0,0 +1,19 @@
+.flip-container {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ justify-content: center;
+}
+
+.flip-side {
+ position: absolute;
+ will-change: transform, opacity;
+ // backface-visibility: hidden;
+}
+
+.flip-front {
+}
+
+.flip-back {
+ // background-color: rgb(223, 223, 223);
+}
diff --git a/src/client/views/nodes/trails/SlideEffect.tsx b/src/client/views/nodes/trails/SlideEffect.tsx
new file mode 100644
index 000000000..00039e3cb
--- /dev/null
+++ b/src/client/views/nodes/trails/SlideEffect.tsx
@@ -0,0 +1,120 @@
+/* eslint-disable react/require-default-props */
+import { animated, to, useInView, useSpring } from '@react-spring/web';
+import React, { useEffect } from 'react';
+import { Doc } from '../../../../fields/Doc';
+import { NumCast } from '../../../../fields/Types';
+import { PresEffect, PresEffectDirection } from './PresEnums';
+import './SlideEffect.scss';
+import { emptyFunction } from '../../../../Utils';
+
+interface SlideEffectProps {
+ doc?: Doc; // pass in doc to extract width, height, bg
+ dir: PresEffectDirection;
+ presEffect: PresEffect;
+ springSettings: {
+ stiffness: number;
+ damping: number;
+ mass: number;
+ };
+ children: React.ReactNode;
+ infinite?: boolean;
+ startOpacity?: number; // set to zero to linearly fade in while animating
+}
+
+const DEFAULT_WIDTH = 40;
+const PREVIEW_OFFSET = 60;
+const ACTUAL_OFFSET = 200;
+
+/**
+ * This component wraps around the doc to create an effect animation, and also wraps the preview animations
+ * for the effects as well.
+ */
+export default function SpringAnimation({ doc, dir, springSettings, presEffect, children, infinite, startOpacity }: SlideEffectProps) {
+ const expandConfig = {
+ to: { scale: 1, x: 0, y: 0 },
+ from: { scale: 0, x: 0, y: 0 },
+ };
+ const fadeConfig = {
+ to: { x: 0, y: 0 },
+ from: { x: 0, y: 0 },
+ };
+ const rotateConfig = {
+ to: { x: 360, y: 0 },
+ from: { x: 0, y: 0 },
+ };
+ const flipConfig = {
+ to: { x: 180, y: 0 },
+ from: { x: 0, y: 0 },
+ };
+ const bounceConfig = {
+ to: { x: 0, y: 0 },
+ from: (() => {
+ const offset = infinite ? PREVIEW_OFFSET : ACTUAL_OFFSET;
+ switch (dir) {
+ case PresEffectDirection.Left: return { x: -offset, y: 0, };
+ case PresEffectDirection.Right: return { x: offset, y: 0, };
+ case PresEffectDirection.Top: return { x: 0, y: -offset, };
+ case PresEffectDirection.Bottom:return { x: 0, y: offset, };
+ default: return { x: 0, y: 0, }; // no movement for center
+ }})(), // prettier-ignore
+ };
+ const rollConfig = {
+ to: { x: 0, y: 0 },
+ from: (() => {
+ switch (dir) {
+ case PresEffectDirection.Left: return { x: -100, y: -120, };
+ case PresEffectDirection.Right: return { x: 100, y: 120, };
+ case PresEffectDirection.Top: return { x: -100, y: -120, };
+ case PresEffectDirection.Bottom: return { x: -100, y: -120, };
+ default: return { x: 0, y: 0, }; // no movement for center
+ }})(), // prettier-ignore
+ };
+
+ // prettier-ignore
+ const effectConfig = (() => {
+ switch (presEffect) {
+ case PresEffect.Fade: return fadeConfig;
+ case PresEffect.Bounce: return bounceConfig;
+ case PresEffect.Rotate: return rotateConfig;
+ case PresEffect.Flip: return flipConfig;
+ case PresEffect.Roll: return rollConfig;
+ case PresEffect.Lightspeed: return { from: {}, to: {} };
+ case PresEffect.Expand:
+ default: return expandConfig;
+ } // prettier-ignore
+ })();
+
+ const [springs, api] = useSpring(
+ () => ({
+ to: { ...effectConfig.to, opacity: 1 },
+ from: { ...effectConfig.from, opacity: startOpacity ?? 1 },
+ config: { tension: springSettings.stiffness, friction: springSettings.damping, mass: springSettings.mass },
+ onStart: emptyFunction,
+ onRest: emptyFunction,
+ }),
+ [springSettings]
+ );
+
+ const [ref, inView] = useInView({
+ once: true,
+ });
+ useEffect(() => {
+ if (inView) {
+ api.start({ loop: infinite, delay: infinite ? 500 : 0 });
+ }
+ }, [inView]);
+ const animatedDiv = (style: any) => (
+ <animated.div ref={ref} style={{ ...style, opacity: to(springs.opacity, val => `${val}`) }}>
+ {children}
+ </animated.div>
+ );
+ const [width, height] = [NumCast(doc?.width, DEFAULT_WIDTH), NumCast(doc?.height, DEFAULT_WIDTH)];
+ const flipAxis = dir === PresEffectDirection.Bottom || dir === PresEffectDirection.Top ? 'X' : 'Y';
+ const [rotateX, rotateY] = flipAxis === 'X' ? ['180deg', undefined] : [undefined, '180deg'];
+ switch (presEffect) {
+ case PresEffect.Flip: return animatedDiv({ transform: to(springs.x, val => `perspective(600px) rotate${flipAxis}(${val}deg)`), width, height, rotateX, rotateY })
+ case PresEffect.Rotate:return animatedDiv({ transform: to(springs.x, val => `rotate(${val}deg)`) });
+ case PresEffect.Roll: return animatedDiv({ transform: to([springs.x, springs.y], (val, val2) => `translate3d(${val}%, 0, 0) rotate3d(0, 0, 1, ${val2}deg)`) });
+ default: return animatedDiv(springs);
+ } // prettier-ignore
+}
diff --git a/src/client/views/nodes/trails/SpringUtils.ts b/src/client/views/nodes/trails/SpringUtils.ts
new file mode 100644
index 000000000..73e1e14f1
--- /dev/null
+++ b/src/client/views/nodes/trails/SpringUtils.ts
@@ -0,0 +1,177 @@
+import { PresEffect, PresEffectDirection, PresMovement } from './PresEnums';
+
+/**
+ * Utilities like enums and interfaces for spring-based transitions.
+ */
+
+export const springPreviewColors = ['rgb(37, 161, 255)', 'rgb(99, 37, 255)', 'rgb(182, 37, 255)', 'rgb(255, 37, 168)'];
+// the type of slide effect timing (spring-driven)
+export enum SpringType {
+ GENTLE = 'gentle',
+ QUICK = 'quick',
+ BOUNCY = 'bouncy',
+ CUSTOM = 'custom',
+}
+
+// settings that control slide effect spring settings
+export interface SpringSettings {
+ type: SpringType;
+ stiffness: number;
+ damping: number;
+ mass: number;
+}
+
+// Overall config
+
+export interface AnimationSettings {
+ effect: PresEffect;
+ direction: PresEffectDirection;
+ stiffness: number;
+ damping: number;
+ mass: number;
+}
+
+// Options in the movement easing dropdown
+export const easeItems = [
+ {
+ text: 'Ease',
+ val: 'ease',
+ },
+ {
+ text: 'Ease In',
+ val: 'ease-in',
+ },
+ {
+ text: 'Ease Out',
+ val: 'ease-out',
+ },
+ {
+ text: 'Ease In Out',
+ val: 'ease-in-out',
+ },
+ {
+ text: 'Linear',
+ val: 'linear',
+ },
+ {
+ text: 'Custom',
+ val: 'custom',
+ },
+];
+
+// Options in the movement type dropdown
+export const movementItems = [
+ { text: 'None', val: PresMovement.None },
+ { text: 'Center', val: PresMovement.Center },
+ { text: 'Zoom', val: PresMovement.Zoom },
+ { text: 'Pan', val: PresMovement.Pan },
+ { text: 'Jump', val: PresMovement.Jump },
+];
+
+// Items in the slide effect dropdown
+export const effectItems = Object.values(PresEffect)
+ .filter(v => isNaN(Number(v)))
+ .map(effect => ({
+ text: effect,
+ val: effect,
+ }));
+
+// Maps each PresEffect to the default timing configuration
+export const presEffectDefaultTimings: {
+ [key: string]: SpringSettings;
+} = {
+ Expand: { type: SpringType.GENTLE, stiffness: 100, damping: 15, mass: 1 },
+ Bounce: {
+ type: SpringType.BOUNCY,
+ stiffness: 600,
+ damping: 15,
+ mass: 1,
+ },
+ Lightspeed: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ Fade: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ Flip: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ Rotate: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ Roll: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ None: {
+ type: SpringType.GENTLE,
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+};
+
+// Dropdown items of timings for the effect
+export const effectTimings = [
+ {
+ text: 'Gentle',
+ val: SpringType.GENTLE,
+ },
+ {
+ text: 'Quick',
+ val: SpringType.QUICK,
+ },
+ {
+ text: 'Bouncy',
+ val: SpringType.BOUNCY,
+ },
+ {
+ text: 'Custom',
+ val: SpringType.CUSTOM,
+ },
+];
+
+// Maps spring names to spring parameters
+export const springMappings: {
+ [key: string]: { stiffness: number; damping: number; mass: number };
+} = {
+ default: {
+ stiffness: 600,
+ damping: 15,
+ mass: 1,
+ },
+ gentle: {
+ stiffness: 100,
+ damping: 15,
+ mass: 1,
+ },
+ quick: {
+ stiffness: 300,
+ damping: 20,
+ mass: 1,
+ },
+ bouncy: {
+ stiffness: 600,
+ damping: 15,
+ mass: 1,
+ },
+ custom: {
+ stiffness: 100,
+ damping: 10,
+ mass: 1,
+ },
+};