diff options
| author | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
|---|---|---|
| committer | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2024-06-03 13:33:37 -0400 |
| commit | 9e77f980e7704999ef0a1c1845d660bccb13ff8a (patch) | |
| tree | 14ca0da5915e4382a7bcb15f7d0b241941c8291f /src/client/views/nodes/trails | |
| parent | 1be63695875c9242fba43d580465e8765cf3991d (diff) | |
| parent | 202e994515392892676f8f080852db1e32b8dbd3 (diff) | |
Merge branch 'master' into nathan-starter
Diffstat (limited to 'src/client/views/nodes/trails')
| -rw-r--r-- | src/client/views/nodes/trails/CubicBezierEditor.tsx | 202 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.scss | 170 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresBox.tsx | 1067 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresElementBox.tsx | 15 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/PresEnums.ts | 2 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/SlideEffect.scss | 19 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/SlideEffect.tsx | 120 | ||||
| -rw-r--r-- | src/client/views/nodes/trails/SpringUtils.ts | 177 |
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, + }, +}; |
