diff --git a/source/Component/ClipList/ClipList.scss b/source/Component/ClipList/ClipList.scss new file mode 100644 index 0000000..94dfb79 --- /dev/null +++ b/source/Component/ClipList/ClipList.scss @@ -0,0 +1,148 @@ +@import "../Theme/Theme.scss"; + +$clip-item-height: 45px; + +div.clip-list-root { + margin: -5px; + display: flex; + flex-wrap: wrap; + + div.clip-item { + margin: 5px; + height: $clip-item-height; + user-select: none; + border-radius: 3px; + overflow: hidden; + cursor: pointer; + display: flex; + + div.clip-item-hole-view { + height: 100%; + width: 10px; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-items: center; + justify-content: space-between; + padding: 5px; + padding-right: 0; + + div.clip-item-hole { + width: 5px; + height: 5px; + background-color: #000000; + flex-shrink: 0; + } + } + + div.clip-icon-view { + width: $clip-item-height; + height: $clip-item-height; + display: flex; + justify-content: center; + align-items: center; + + i.icon { + display: inline-block; + font-size: 25px; + } + + i.delete { + display: none; + } + + i.delete:hover { + color: $lt-red; + } + } + + div.clip-item-content { + width: calc( 100% - 65px ); + padding-right: 10px; + max-width: 125px; + height: $clip-item-height; + display: flex; + flex-direction: column; + justify-content: center; + + div { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 100%; + } + + div.info { + opacity: .75; + } + } + } + + div.clip-item.disable { + cursor: not-allowed; + } + + div.clip-item.able:hover { + + div.clip-icon-view { + + i.icon { + display: none; + } + + i.delete { + display: inline-block; + } + } + } + + div.add-button { + width: 26px; + height: 26px; + display: flex; + justify-content: center; + align-items: center; + } +} + +div.dark.clip-list-root { + + div.clip-item { + background-color: $lt-bg-color-lvl3-dark; + + div.clip-item-hole-view div.clip-item-hole { + background-color: $lt-bg-color-lvl4-dark; + } + } + + div.clip-item.able:hover { + color: $lt-font-color-lvl2-dark; + background-color: $lt-bg-color-lvl2-dark; + } + + div.clip-item.focus { + color: $lt-font-color-lvl1-dark; + background-color: $lt-bg-color-lvl1-dark; + } +} + +div.light.clip-list-root { + + div.clip-item { + background-color: $lt-bg-color-lvl3-light; + + div.clip-item-hole-view div.clip-item-hole { + background-color: $lt-bg-color-lvl4-light; + } + } + + div.clip-item.able:hover { + color: $lt-font-color-lvl2-light; + background-color: $lt-bg-color-lvl2-light; + } + + div.clip-item.focus { + color: $lt-font-color-lvl1-light; + background-color: $lt-bg-color-lvl1-light; + } +} \ No newline at end of file diff --git a/source/Component/ClipList/ClipList.tsx b/source/Component/ClipList/ClipList.tsx new file mode 100644 index 0000000..bb19940 --- /dev/null +++ b/source/Component/ClipList/ClipList.tsx @@ -0,0 +1,130 @@ +import { Localization } from "@Component/Localization/Localization"; +import { Theme } from "@Component/Theme/Theme"; +import { Icon } from "@fluentui/react"; +import { Clip } from "@Model/Clip"; +import { Component, ReactNode } from "react"; +import "./ClipList.scss"; + +interface IClipListProps { + clips: Clip[]; + focus?: Clip; + disable?: boolean; + add?: () => any; + click?: (clip: Clip) => any; + delete?: (clip: Clip) => any; +} + +class ClipList extends Component { + + private isInnerClick: boolean = false; + + private resolveCallback(fn?: (p: any) => any, p?: any): any { + if (this.props.disable) { + return false; + } + if (fn) { + return fn(p); + } + } + + private parseTime(time?: number): string { + if (time === undefined) { + return "0:0:0:0"; + } + const h = Math.floor(time / 3600); + const m = Math.floor((time % 3600) / 60); + const s = Math.floor((time % 3600) % 60); + const ms = Math.floor((time % 1) * 1000); + return `${h}:${m}:${s}:${ms}`; + } + + private getClipInfo(clip: Clip): string { + let fps = Math.floor(clip.frames.length / clip.time); + if (isNaN(fps)) fps = 0; + return `${this.parseTime(clip.time)} ${fps}fps`; + } + + private renderClip(clip: Clip) { + + const focus = clip.equal(this.props.focus); + const disable = this.props.disable; + const classList = ["clip-item"]; + + if (focus) { + classList.push("focus"); + } + + if (disable) { + classList.push("disable"); + } else { + classList.push("able"); + } + + return
{ + if (this.isInnerClick) { + this.isInnerClick = false; + } else { + this.resolveCallback(this.props.click, clip); + } + }} + > +
+ {new Array(4).fill(0).map((_, index) => { + return
+ })} +
+
+ + { + this.isInnerClick = true; + this.resolveCallback(this.props.delete, clip); + }} + /> +
+
+
{clip.name}
+
{ + clip.isRecording ? + : + this.getClipInfo(clip) + }
+
+
; + } + + private renderAddButton(): ReactNode { + + const classList = ["clip-item", "add-button"]; + + if (this.props.disable) { + classList.push("disable"); + } else { + classList.push("able"); + } + + return
this.resolveCallback(this.props.add)} + > + +
+ } + + public render(): ReactNode { + return + { this.props.clips.map((clip => { + return this.renderClip(clip); + })) } + { this.renderAddButton() } + ; + } +} + +export { ClipList }; \ No newline at end of file diff --git a/source/Component/Recorder/Recorder.scss b/source/Component/Recorder/Recorder.scss new file mode 100644 index 0000000..2f03595 --- /dev/null +++ b/source/Component/Recorder/Recorder.scss @@ -0,0 +1,155 @@ +@import "../Theme/Theme.scss"; + +div.recorder-root { + width: 100%; + box-sizing: border-box; + padding: 10px 10px 0 10px; + + div.recorder-slider { + width: 100%; + + div.ms-Slider-slideBox { + height: 16px; + } + + span.ms-Slider-thumb { + width: 12px; + height: 12px; + line-height: 16px; + border-width: 3px; + top: -4px; + } + + span.ms-Slider-active { + height: 3px; + } + + span.ms-Slider-inactive { + height: 3px; + } + } + + div.recorder-slider.disable { + opacity: .6; + } + + div.recorder-content { + width: 100%; + height: 32px; + display: flex; + justify-content: space-between; + align-items: center; + box-sizing: border-box; + padding: 0 8px; + + div.time-view { + flex-shrink: 1; + width: 50%; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + div.ctrl-button { + cursor: pointer; + user-select: none; + width: 96px; + flex-shrink: 0; + text-align: center; + display: flex; + justify-content: center; + align-items: center; + + div.ctrl-action { + width: 32px; + height: 32px; + display: flex; + justify-content: center; + align-items: center; + } + + div.ctrl-action-main { + font-size: 1.5em; + } + + div.ctrl-action.disable { + cursor: not-allowed; + opacity: .6; + } + } + + div.speed-view { + flex-shrink: 1; + width: 50%; + text-align: right; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } +} + +div.recorder-root.light { + + div.recorder-slider { + + span.ms-Slider-thumb { + background-color: $lt-bg-color-lvl1-light; + border-color: $lt-font-color-normal-light; + } + + span.ms-Slider-active { + background-color: $lt-font-color-normal-light; + } + + span.ms-Slider-inactive { + background-color: $lt-bg-color-lvl1-light; + } + } + + div.recorder-content { + + div.ctrl-button div.ctrl-action:hover { + background-color: $lt-bg-color-lvl3-light; + color: $lt-font-color-lvl1-light; + } + + div.ctrl-button div.ctrl-action.disable:hover { + background-color: $lt-bg-color-lvl4-light; + color: $lt-font-color-normal-light; + } + } +} + +div.recorder-root.dark { + + div.recorder-slider { + + span.ms-Slider-thumb { + background-color: $lt-bg-color-lvl1-dark; + border-color: $lt-font-color-normal-dark; + } + + span.ms-Slider-active { + background-color: $lt-font-color-normal-dark; + } + + span.ms-Slider-inactive { + background-color: $lt-bg-color-lvl1-dark; + } + } + + div.recorder-content { + + div.ctrl-button div.ctrl-action:hover { + background-color: $lt-bg-color-lvl3-dark; + color: $lt-font-color-lvl1-dark; + } + + div.ctrl-button div.ctrl-action.disable:hover { + background-color: $lt-bg-color-lvl4-dark; + color: $lt-font-color-normal-dark; + } + } +} \ No newline at end of file diff --git a/source/Component/Recorder/Recorder.tsx b/source/Component/Recorder/Recorder.tsx new file mode 100644 index 0000000..d75e3d9 --- /dev/null +++ b/source/Component/Recorder/Recorder.tsx @@ -0,0 +1,116 @@ +import { Localization } from "@Component/Localization/Localization"; +import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme"; +import { Icon, Slider } from "@fluentui/react"; +import { Component, ReactNode } from "react"; +import "./Recorder.scss"; + +interface IRecorderProps { + mode: "P" | "R", + running?: boolean, + name?: string; + fps?: number; + allFrame?: number; + currentFrame?: number; + allTime?: number; + currentTime?: number; + action?: () => void; +} + +class Recorder extends Component { + + private parseTime(time?: number): string { + if (time === undefined) { + return "0:0:0:0"; + } + const h = Math.floor(time / 3600); + const m = Math.floor((time % 3600) / 60); + const s = Math.floor((time % 3600) % 60); + const ms = Math.floor((time % 1) * 1000); + return `${h}:${m}:${s}:${ms}`; + } + + private getRecordInfo(): ReactNode { + if (this.props.mode === "P") { + return ; + } + else if (this.props.mode === "R") { + return ; + } + } + + private getActionIcon(): string { + if (this.props.mode === "P") { + if (this.props.running) { + return "Pause"; + } else { + return "Play"; + } + } + else if (this.props.mode === "R") { + if (this.props.running) { + return "Stop"; + } else { + return "StatusCircleRing"; + } + } + return "Play"; + } + + public render(): ReactNode { + + const isSliderDisable = this.props.mode === "R"; + const isJumpDisable = this.props.mode === "R"; + + return + +
+
+ {this.getRecordInfo()} +
+
+
+ +
+
+ +
+
+ +
+
+
+ { + this.props.name ? + {this.props.name} : + + } +
+
+
; + } +} + +export { Recorder }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 0a7d1d8..2ac2e14 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -14,6 +14,7 @@ import { PopupController } from "@Context/Popups"; import { Behavior } from "@Model/Behavior"; import { IParameter, IParamValue } from "@Model/Parameter"; import { Actuator } from "@Model/Actuator"; +import { Clip } from "@Model/Clip"; function randomColor(unNormal: boolean = false) { const color = [ @@ -35,14 +36,17 @@ interface IStatusEvent { fileChange: void; renderLoop: number; physicsLoop: number; + recordLoop: number; mouseModChange: void; focusObjectChange: void; focusLabelChange: void; focusBehaviorChange: void; objectChange: void; + focusClipChange: void; rangeLabelChange: void; groupLabelChange: void; groupBehaviorChange: void; + clipChange: void; labelChange: void; rangeAttrChange: void; labelAttrChange: void; @@ -98,6 +102,11 @@ class Status extends Emitter { */ public focusBehavior?: Behavior; + /** + * 焦点行为 + */ + public focusClip?: Clip; + private drawTimer?: NodeJS.Timeout; private delayDraw = () => { @@ -119,11 +128,13 @@ class Status extends Emitter { // 循环事件 this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) }); + this.actuator.on("record", (t) => { this.emit("recordLoop", t) }); // 对象变化事件 this.model.on("objectChange", () => this.emit("objectChange")); this.model.on("labelChange", () => this.emit("labelChange")); this.model.on("behaviorChange", () => this.emit("behaviorChange")); + this.model.on("clipChange", () => this.emit("clipChange")); // 弹窗事件 this.popup.on("popupChange", () => this.emit("popupChange")); @@ -220,6 +231,16 @@ class Status extends Emitter { this.emit("focusBehaviorChange"); } + /** + * 更新焦点行为 + */ + public setClipObject(clip?: Clip) { + if (this.focusClip !== clip) { + this.focusClip = clip; + } + this.emit("focusClipChange"); + } + /** * 修改范围属性 */ @@ -403,6 +424,22 @@ class Status extends Emitter { return label; } + public newClip() { + let searchKey = I18N(this.setting.language, "Object.List.New.Clip", { id: "" }); + let nextIndex = 1; + this.model.clipPool.forEach((obj) => { + nextIndex = Math.max(nextIndex, this.getNextNumber( + obj.name, searchKey + )); + }); + const clip = this.model.addClip( + I18N(this.setting.language, "Object.List.New.Clip", { + id: nextIndex.toString() + }) + ); + return clip; + } + public setMouseMod(mod: MouseMod) { this.mouseMod = mod; if (this.renderer instanceof ClassicRenderer) { diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index 2138ff9..d748ce2 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -31,6 +31,7 @@ const EN_US = { "Object.List.New.Group": "Group object {id}", "Object.List.New.Range": "Range object {id}", "Object.List.New.Label": "Label {id}", + "Object.List.New.Clip": "Clip {id}", "Object.List.No.Data": "There are no objects in the model, click the button to create it", "Object.Picker.List.No.Data": "There is no model in the model for this option", "Behavior.Picker.Add.Button": "Click here to assign behavior to this group", @@ -53,6 +54,11 @@ const EN_US = { "Panel.Info.Behavior.List.View": "Edit view behavior list", "Panel.Title.Behavior.Details.View": "Behavior", "Panel.Info.Behavior.Details.View": "Edit view Behavior attributes", + "Panel.Title.Behavior.Clip.Player": "Recording", + "Panel.Info.Behavior.Clip.Player": "Pre render recorded data", + "Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps}fps", + "Panel.Info.Behavior.Clip.Record.Formate": "Record: {time}", + "Panel.Info.Behavior.Clip.Uname.Clip": "Waiting for recording...", "Popup.Title.Unnamed": "Popup message", "Popup.Title.Confirm": "Confirm message", "Popup.Action.Yes": "Confirm", @@ -143,6 +149,7 @@ const EN_US = { "Panel.Info.Behavior.Details.Parameter.Key.Vec.X": "{key} X", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z", + "Panel.Info.Clip.List.Error.Nodata": "There is no clip, please click the record button to record, or click the plus sign to create", "Info.Hint.Save.After.Close": "Any unsaved progress will be lost. Are you sure you want to continue?", "Info.Hint.Load.File.Title": "Load save", "Info.Hint.Load.File.Intro": "Release to load the dragged save file", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 1ee7e39..77c55ea 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -31,6 +31,7 @@ const ZH_CN = { "Object.List.New.Group": "群对象 {id}", "Object.List.New.Range": "范围对象 {id}", "Object.List.New.Label": "标签 {id}", + "Object.List.New.Clip": "剪辑片段 {id}", "Object.List.No.Data": "模型中没有任何对象,点击按钮以创建", "Object.Picker.List.No.Data": "模型中没有合适此选项的模型", "Behavior.Picker.Add.Button": "点击此处以赋予行为到此群", @@ -53,6 +54,11 @@ const ZH_CN = { "Panel.Info.Behavior.List.View": "编辑查看行为列表", "Panel.Title.Behavior.Details.View": "行为", "Panel.Info.Behavior.Details.View": "编辑查看行为属性", + "Panel.Title.Behavior.Clip.Player": "录制", + "Panel.Info.Behavior.Clip.Player": "预渲染录制数据", + "Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps} fps", + "Panel.Info.Behavior.Clip.Record.Formate": "录制: {time}", + "Panel.Info.Behavior.Clip.Uname.Clip": "等待录制...", "Popup.Title.Unnamed": "弹窗消息", "Popup.Title.Confirm": "确认消息", "Popup.Action.Yes": "确定", @@ -143,6 +149,7 @@ const ZH_CN = { "Panel.Info.Behavior.Details.Parameter.Key.Vec.X": "{key} X 坐标", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y 坐标", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标", + "Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建", "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", "Info.Hint.Load.File.Title": "加载存档", "Info.Hint.Load.File.Intro": "释放以加载拽入的存档", diff --git a/source/Model/Actuator.ts b/source/Model/Actuator.ts index 0417357..cc57d27 100644 --- a/source/Model/Actuator.ts +++ b/source/Model/Actuator.ts @@ -1,8 +1,17 @@ import { Model } from "@Model/Model"; import { Emitter } from "@Model/Emitter"; +import { Clip } from "@Model/Clip"; + +enum ActuatorModel { + Play = 1, + Record = 2, + View = 3, + Offline = 4 +} interface IActuatorEvent { startChange: boolean; + record: number; loop: number; } @@ -26,6 +35,47 @@ class Actuator extends Emitter { */ private startFlag: boolean = false; + /** + * 模式 + */ + public mod: ActuatorModel = ActuatorModel.View; + + /** + * 录制剪辑 + */ + public recordClip?: Clip; + + /** + * 开始录制 + */ + public startRecord(clip: Clip) { + + // 记录录制片段 + this.recordClip = clip; + clip.isRecording = true; + + // 如果仿真未开启,开启仿真 + if (!this.start()) this.start(true); + + // 设置状态 + this.mod = ActuatorModel.Record; + } + + /** + * 结束录制 + */ + public endRecord() { + + this.recordClip && (this.recordClip.isRecording = false); + this.recordClip = undefined; + + // 如果仿真未停止,停止仿真 + if (this.start()) this.start(false); + + // 设置状态 + this.mod = ActuatorModel.View; + } + /** * 主时钟状态控制 */ @@ -72,13 +122,30 @@ class Actuator extends Emitter { } else { this.alignTimer += durTime; if (this.alignTimer > (1 / this.fps)) { + + // 更新模型 this.model.update(this.alignTimer * this.speed); + + // 绘制模型 + this.model.draw(); + + // 录制模型 + if ( + this.mod === ActuatorModel.Record || + this.mod === ActuatorModel.Offline + ) { + this.recordClip?.record(this.alignTimer * this.speed); + this.emit("record", this.alignTimer); + } + this.emit("loop", this.alignTimer); this.alignTimer = 0; } } } - } else { + } + + else { this.emit("loop", Infinity); } } @@ -122,4 +189,4 @@ class Actuator extends Emitter { } } -export { Actuator } \ No newline at end of file +export { Actuator, ActuatorModel } \ No newline at end of file diff --git a/source/Model/Clip.ts b/source/Model/Clip.ts new file mode 100644 index 0000000..64008b8 --- /dev/null +++ b/source/Model/Clip.ts @@ -0,0 +1,105 @@ +import { IAnyObject, Model } from "@Model/Model"; +import { v4 as uuid } from "uuid"; +import { Group } from "@Model/Group"; +import { Range } from "@Model/Range"; + +interface IDrawCommand { + type: "points" | "cube"; + id: string; + data?: Float32Array; + position?: number[]; + radius?: number[]; + parameter?: IAnyObject; +} + +interface IFrame { + commands: IDrawCommand[]; + duration: number; +} + +/** + * 剪辑片段 + */ +class Clip { + + public id: string; + + /** + * 时间 + */ + public time: number = 0; + + /** + * 用户自定义名称 + */ + public name: string = ""; + + /** + * 模型 + */ + public model: Model; + + /** + * 全部帧 + */ + public frames: IFrame[] = []; + + /** + * 是否正在录制 + */ + public isRecording: boolean = false; + + /** + * 录制一帧 + */ + public record(t: number): IFrame { + const commands: IDrawCommand[] = []; + + for (let i = 0; i < this.model.objectPool.length; i++) { + + let object = this.model.objectPool[i]; + object.renderParameter.color = object.color; + + if (object.display && object instanceof Group) { + commands.push({ + type: "points", + id: object.id, + data: object.exportPositionData(), + parameter: object.renderParameter + }); + } + + + if (object.display && object instanceof Range) { + commands.push({ + type: "cube", + id: object.id, + position: object.position, + radius: object.radius, + parameter: object.renderParameter + }); + } + } + + const frame: IFrame = { + commands: commands, + duration: t + }; + + this.time += t; + this.frames.push(frame); + + return frame; + } + + public equal(clip?: Clip) { + return clip === this || clip?.id === this.id; + } + + public constructor(model: Model) { + this.model = model; + this.id = uuid(); + } +} + +export { Clip }; \ No newline at end of file diff --git a/source/Model/Model.ts b/source/Model/Model.ts index 1ac2e2a..83012fa 100644 --- a/source/Model/Model.ts +++ b/source/Model/Model.ts @@ -5,6 +5,7 @@ import { IParamValue } from "@Model/Parameter"; import { CtrlObject } from "@Model/CtrlObject"; import { Emitter } from "@Model/Emitter"; import { AbstractRenderer } from "@Model/Renderer"; +import { Clip } from "@Model/Clip"; import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "@Model/Behavior"; /** @@ -22,6 +23,7 @@ type ModelEvent = { objectChange: CtrlObject[]; individualChange: Group; behaviorChange: IAnyBehavior; + clipChange: Clip[]; }; /** @@ -330,6 +332,51 @@ class Model extends Emitter { } } + /** + * 剪辑数据 + */ + public clipPool: Clip[] = []; + + /** + * 新建剪辑片段 + */ + public addClip(name?: string): Clip { + let newClip = new Clip(this); + newClip.name = name ?? ""; + this.clipPool.push(newClip); + console.log(`Model: Create clip ${name ?? newClip.id}`); + this.emit("clipChange", this.clipPool); + return newClip; + } + + /** + * 删除一个剪辑片段 + */ + public deleteClip(name: ObjectID | Clip) { + let deletedClip: Clip | undefined; + let index = 0; + + for (let i = 0; i < this.clipPool.length; i++) { + if (name instanceof Clip) { + if (this.clipPool[i].equal(name)) { + deletedClip = this.clipPool[i]; + index = i; + } + } + + else if (name === this.clipPool[i].id) { + deletedClip = this.clipPool[i]; + index = i; + } + } + + if (deletedClip) { + this.behaviorPool.splice(index, 1); + console.log(`Model: Delete clip ${deletedClip.name ?? deletedClip.id}`); + this.emit("clipChange", this.clipPool); + } + } + /** * 渲染器 */ @@ -372,8 +419,6 @@ class Model extends Emitter { object.runner(t, "finalEffect"); } } - - this.draw(); } public draw() { diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 255b201..cfdfd1f 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -55,7 +55,7 @@ class SimulatorWeb extends Component { items: [ {panels: ["RenderView"]}, { - items: [{panels: ["BehaviorList"]}, {panels: ["LabelList"]}], + items: [{panels: ["ClipPlayer", "BehaviorList"]}, {panels: ["LabelList"]}], scale: 80, layout: LayoutDirection.X } diff --git a/source/Panel/ClipPlayer/ClipPlayer.scss b/source/Panel/ClipPlayer/ClipPlayer.scss new file mode 100644 index 0000000..abfe431 --- /dev/null +++ b/source/Panel/ClipPlayer/ClipPlayer.scss @@ -0,0 +1,6 @@ +div.Clip-player-clip-list-root { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 10px; +} \ No newline at end of file diff --git a/source/Panel/ClipPlayer/ClipPlayer.tsx b/source/Panel/ClipPlayer/ClipPlayer.tsx new file mode 100644 index 0000000..79feaa4 --- /dev/null +++ b/source/Panel/ClipPlayer/ClipPlayer.tsx @@ -0,0 +1,42 @@ +import { Component, ReactNode } from "react"; +import { ClipList } from "@Component/ClipList/ClipList"; +import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { Theme } from "@Component/Theme/Theme"; +import { Message } from "@Input/Message/Message"; +import { Clip } from "@Model/Clip"; +import { ActuatorModel } from "@Model/Actuator"; +import "./ClipPlayer.scss"; + +@useStatusWithEvent("clipChange", "focusClipChange", "actuatorStartChange") +class ClipPlayer extends Component { + + private renderMessage(): ReactNode { + return ; + } + + private renderClipList(clipList: Clip[]): ReactNode { + + const disable = + !this.props.status?.focusClip && + ( + this.props.status?.actuator.mod === ActuatorModel.Record || + this.props.status?.actuator.mod === ActuatorModel.Offline + ); + + return ; + } + + public render(): ReactNode { + const clipList = this.props.status?.model.clipPool ?? []; + + return + { clipList.length > 0 ? null : this.renderMessage() } + { this.renderClipList(clipList) } + ; + } +} + +export { ClipPlayer }; \ No newline at end of file diff --git a/source/Panel/ClipPlayer/ClipRecorder.tsx b/source/Panel/ClipPlayer/ClipRecorder.tsx new file mode 100644 index 0000000..8115f5d --- /dev/null +++ b/source/Panel/ClipPlayer/ClipRecorder.tsx @@ -0,0 +1,59 @@ +import { Component, ReactNode } from "react"; +import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { Recorder } from "@Component/Recorder/Recorder"; +import { ActuatorModel } from "@Model/Actuator"; + +@useStatusWithEvent("actuatorStartChange", "focusClipChange", "recordLoop") +class ClipRecorder extends Component { + public render(): ReactNode { + + let mod: "P" | "R" = this.props.status?.focusClip ? "P" : "R"; + let runner: boolean = false; + let currentTime: number = 0; + + // 是否开始录制 + if (mod === "R") { + + // 是否正在录制 + runner = this.props.status?.actuator.mod === ActuatorModel.Record || + this.props.status?.actuator.mod === ActuatorModel.Offline; + + currentTime = this.props.status?.actuator.recordClip?.time ?? 0; + } + + else if (mod === "P") { + + // 是否正在播放 + runner = this.props.status?.actuator.mod === ActuatorModel.Play; + } + + return { + + // 开启录制 + if (mod === "R" && !runner) { + + // 获取新实例 + let newClip = this.props.status?.newClip(); + + // 开启录制时钟 + this.props.status?.actuator.startRecord(newClip!); + console.log("ClipRecorder: Rec start..."); + } + + // 暂停录制 + if (mod === "R" && runner) { + + // 暂停录制时钟 + this.props.status?.actuator.endRecord(); + console.log("ClipRecorder: Rec end..."); + } + }} + /> + } +} + +export { ClipRecorder }; \ No newline at end of file diff --git a/source/Panel/Panel.tsx b/source/Panel/Panel.tsx index c7ba6fc..ef3542b 100644 --- a/source/Panel/Panel.tsx +++ b/source/Panel/Panel.tsx @@ -10,6 +10,8 @@ import { LabelDetails } from "@Panel/LabelDetails/LabelDetails"; import { GroupDetails } from "@Panel/GroupDetails/GroupDetails"; import { BehaviorList } from "@Panel/BehaviorList/BehaviorList"; import { BehaviorDetails } from "@Panel/BehaviorDetails/BehaviorDetails"; +import { ClipPlayer } from "@Panel/ClipPlayer/ClipPlayer"; +import { ClipRecorder } from "@Panel/ClipPlayer/ClipRecorder"; interface IPanelInfo { nameKey: string; @@ -31,6 +33,7 @@ type PanelId = "" | "GroupDetails" // 群属性 | "BehaviorList" // 行为列表 | "BehaviorDetails" // 行为属性 +| "ClipPlayer" // 剪辑影片 ; const PanelInfoMap = new Map(); @@ -66,6 +69,10 @@ PanelInfoMap.set("BehaviorDetails", { nameKey: "Panel.Title.Behavior.Details.View", introKay: "Panel.Info.Behavior.Details.View", class: BehaviorDetails }); +PanelInfoMap.set("ClipPlayer", { + nameKey: "Panel.Title.Behavior.Clip.Player", introKay: "Panel.Info.Behavior.Clip.Player", + class: ClipPlayer, header: ClipRecorder, hidePadding: true +}); function getPanelById(panelId: PanelId): ReactNode { switch (panelId) {