Add clip model & clipList component & recoder panel #48
							
								
								
									
										148
									
								
								source/Component/ClipList/ClipList.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								source/Component/ClipList/ClipList.scss
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										130
									
								
								source/Component/ClipList/ClipList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								source/Component/ClipList/ClipList.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<IClipListProps> { | ||||||
|  | 
 | ||||||
|  | 	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 <div | ||||||
|  | 			key={clip.id} | ||||||
|  | 			className={classList.join(" ")} | ||||||
|  | 			onClick={() => { | ||||||
|  | 				if (this.isInnerClick) { | ||||||
|  | 					this.isInnerClick = false; | ||||||
|  | 				} else { | ||||||
|  | 					this.resolveCallback(this.props.click, clip); | ||||||
|  | 				} | ||||||
|  | 			}} | ||||||
|  | 		> | ||||||
|  | 			<div className="clip-item-hole-view"> | ||||||
|  | 				{new Array(4).fill(0).map((_, index) => { | ||||||
|  | 					return <div className="clip-item-hole" key={index}/> | ||||||
|  | 				})} | ||||||
|  | 			</div> | ||||||
|  | 			<div className="clip-icon-view"> | ||||||
|  | 				<Icon iconName="MyMoviesTV" className="icon"/> | ||||||
|  | 				<Icon | ||||||
|  | 					iconName="Delete" | ||||||
|  | 					className="delete" | ||||||
|  | 					onClick={() => { | ||||||
|  | 						this.isInnerClick = true; | ||||||
|  | 						this.resolveCallback(this.props.delete, clip); | ||||||
|  | 					}} | ||||||
|  | 				/> | ||||||
|  | 			</div> | ||||||
|  | 			<div className="clip-item-content"> | ||||||
|  | 				<div className="title">{clip.name}</div> | ||||||
|  | 				<div className="info">{ | ||||||
|  | 					clip.isRecording ? | ||||||
|  | 						<Localization i18nKey="Panel.Info.Behavior.Clip.Uname.Clip"/> : | ||||||
|  | 						this.getClipInfo(clip) | ||||||
|  | 				}</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div>; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private renderAddButton(): ReactNode { | ||||||
|  | 
 | ||||||
|  | 		const classList = ["clip-item", "add-button"]; | ||||||
|  | 
 | ||||||
|  | 		if (this.props.disable) { | ||||||
|  | 			classList.push("disable"); | ||||||
|  | 		} else { | ||||||
|  | 			classList.push("able"); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return <div | ||||||
|  | 			key="ADD_BUTTON" | ||||||
|  | 			className={classList.join(" ")} | ||||||
|  | 			onClick={() => this.resolveCallback(this.props.add)} | ||||||
|  | 		> | ||||||
|  |             <Icon iconName="Add"/> | ||||||
|  |         </div> | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public render(): ReactNode { | ||||||
|  | 		return <Theme className="clip-list-root"> | ||||||
|  | 			{ this.props.clips.map((clip => { | ||||||
|  | 				return this.renderClip(clip); | ||||||
|  | 			})) } | ||||||
|  | 			{ this.renderAddButton() } | ||||||
|  | 		</Theme>; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { ClipList }; | ||||||
							
								
								
									
										155
									
								
								source/Component/Recorder/Recorder.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								source/Component/Recorder/Recorder.scss
									
									
									
									
									
										Normal file
									
								
							| @ -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; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										116
									
								
								source/Component/Recorder/Recorder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								source/Component/Recorder/Recorder.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<IRecorderProps> { | ||||||
|  | 
 | ||||||
|  | 	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 <Localization | ||||||
|  | 				i18nKey="Panel.Info.Behavior.Clip.Time.Formate" | ||||||
|  | 				options={{ | ||||||
|  | 					current: this.parseTime(this.props.currentTime), | ||||||
|  | 					all: this.parseTime(this.props.allTime), | ||||||
|  | 					fps: this.props.fps ? this.props.fps.toString() : "0" | ||||||
|  | 				}} | ||||||
|  | 			/>; | ||||||
|  | 		} | ||||||
|  | 		else if (this.props.mode === "R") { | ||||||
|  | 			return <Localization | ||||||
|  | 				i18nKey="Panel.Info.Behavior.Clip.Record.Formate" | ||||||
|  | 				options={{ | ||||||
|  | 					time: this.parseTime(this.props.currentTime), | ||||||
|  | 				}} | ||||||
|  | 			/>; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 <Theme | ||||||
|  | 			className="recorder-root" | ||||||
|  | 			backgroundLevel={BackgroundLevel.Level4} | ||||||
|  | 			fontLevel={FontLevel.normal} | ||||||
|  | 		> | ||||||
|  | 			<Slider | ||||||
|  | 				min={0} | ||||||
|  | 				disabled={isSliderDisable} | ||||||
|  | 				value={this.props.currentFrame} | ||||||
|  | 				max={this.props.allFrame} | ||||||
|  | 				className={"recorder-slider" + (isSliderDisable ? " disable" : "")} | ||||||
|  | 				showValue={false} | ||||||
|  | 			/> | ||||||
|  | 			<div className="recorder-content"> | ||||||
|  | 				<div className="time-view"> | ||||||
|  | 					{this.getRecordInfo()} | ||||||
|  | 				</div> | ||||||
|  | 				<div className="ctrl-button"> | ||||||
|  | 					<div className={"ctrl-action" + (isJumpDisable ? " disable" : "")}> | ||||||
|  | 						<Icon iconName="Back"/> | ||||||
|  | 					</div> | ||||||
|  | 					<div className="ctrl-action ctrl-action-main" onClick={this.props.action}> | ||||||
|  | 						<Icon iconName={this.getActionIcon()}/> | ||||||
|  | 					</div> | ||||||
|  | 					<div className={"ctrl-action" + (isJumpDisable ? " disable" : "")}> | ||||||
|  | 						<Icon iconName="Forward"/> | ||||||
|  | 					</div> | ||||||
|  | 				</div> | ||||||
|  | 				<div className="speed-view"> | ||||||
|  | 					{ | ||||||
|  | 						this.props.name ? | ||||||
|  | 							<span>{this.props.name}</span> : | ||||||
|  | 							<Localization i18nKey="Panel.Info.Behavior.Clip.Uname.Clip"/> | ||||||
|  | 					} | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</Theme>; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { Recorder }; | ||||||
| @ -14,6 +14,7 @@ import { PopupController } from "@Context/Popups"; | |||||||
| import { Behavior } from "@Model/Behavior"; | import { Behavior } from "@Model/Behavior"; | ||||||
| import { IParameter, IParamValue } from "@Model/Parameter"; | import { IParameter, IParamValue } from "@Model/Parameter"; | ||||||
| import { Actuator } from "@Model/Actuator"; | import { Actuator } from "@Model/Actuator"; | ||||||
|  | import { Clip } from "@Model/Clip"; | ||||||
| 
 | 
 | ||||||
| function randomColor(unNormal: boolean = false) { | function randomColor(unNormal: boolean = false) { | ||||||
|     const color = [ |     const color = [ | ||||||
| @ -35,14 +36,17 @@ interface IStatusEvent { | |||||||
|     fileChange: void; |     fileChange: void; | ||||||
|     renderLoop: number; |     renderLoop: number; | ||||||
|     physicsLoop: number; |     physicsLoop: number; | ||||||
|  |     recordLoop: number; | ||||||
|     mouseModChange: void; |     mouseModChange: void; | ||||||
|     focusObjectChange: void; |     focusObjectChange: void; | ||||||
|     focusLabelChange: void; |     focusLabelChange: void; | ||||||
|     focusBehaviorChange: void; |     focusBehaviorChange: void; | ||||||
|     objectChange: void; |     objectChange: void; | ||||||
|  |     focusClipChange: void; | ||||||
|     rangeLabelChange: void; |     rangeLabelChange: void; | ||||||
|     groupLabelChange: void; |     groupLabelChange: void; | ||||||
|     groupBehaviorChange: void; |     groupBehaviorChange: void; | ||||||
|  |     clipChange: void; | ||||||
|     labelChange: void; |     labelChange: void; | ||||||
|     rangeAttrChange: void; |     rangeAttrChange: void; | ||||||
|     labelAttrChange: void; |     labelAttrChange: void; | ||||||
| @ -98,6 +102,11 @@ class Status extends Emitter<IStatusEvent> { | |||||||
|      */ |      */ | ||||||
|     public focusBehavior?: Behavior; |     public focusBehavior?: Behavior; | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 焦点行为 | ||||||
|  |      */ | ||||||
|  |     public focusClip?: Clip; | ||||||
|  | 
 | ||||||
|     private drawTimer?: NodeJS.Timeout; |     private drawTimer?: NodeJS.Timeout; | ||||||
| 
 | 
 | ||||||
|     private delayDraw = () => { |     private delayDraw = () => { | ||||||
| @ -119,11 +128,13 @@ class Status extends Emitter<IStatusEvent> { | |||||||
| 
 | 
 | ||||||
|         // 循环事件
 |         // 循环事件
 | ||||||
|         this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) }); |         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("objectChange", () => this.emit("objectChange")); | ||||||
|         this.model.on("labelChange", () => this.emit("labelChange")); |         this.model.on("labelChange", () => this.emit("labelChange")); | ||||||
|         this.model.on("behaviorChange", () => this.emit("behaviorChange")); |         this.model.on("behaviorChange", () => this.emit("behaviorChange")); | ||||||
|  |         this.model.on("clipChange", () => this.emit("clipChange")); | ||||||
| 
 | 
 | ||||||
|         // 弹窗事件
 |         // 弹窗事件
 | ||||||
|         this.popup.on("popupChange", () => this.emit("popupChange")); |         this.popup.on("popupChange", () => this.emit("popupChange")); | ||||||
| @ -220,6 +231,16 @@ class Status extends Emitter<IStatusEvent> { | |||||||
|         this.emit("focusBehaviorChange"); |         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<IStatusEvent> { | |||||||
|         return label; |         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) { |     public setMouseMod(mod: MouseMod) { | ||||||
|         this.mouseMod = mod; |         this.mouseMod = mod; | ||||||
|         if (this.renderer instanceof ClassicRenderer) { |         if (this.renderer instanceof ClassicRenderer) { | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ const EN_US = { | |||||||
|     "Object.List.New.Group": "Group object {id}", |     "Object.List.New.Group": "Group object {id}", | ||||||
|     "Object.List.New.Range": "Range object {id}", |     "Object.List.New.Range": "Range object {id}", | ||||||
|     "Object.List.New.Label": "Label {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.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", |     "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", |     "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.Info.Behavior.List.View": "Edit view behavior list", | ||||||
|     "Panel.Title.Behavior.Details.View": "Behavior", |     "Panel.Title.Behavior.Details.View": "Behavior", | ||||||
|     "Panel.Info.Behavior.Details.View": "Edit view Behavior attributes", |     "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.Unnamed": "Popup message", | ||||||
|     "Popup.Title.Confirm": "Confirm message", |     "Popup.Title.Confirm": "Confirm message", | ||||||
|     "Popup.Action.Yes": "Confirm", |     "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.X": "{key} X", | ||||||
|     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y", |     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y", | ||||||
|     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z", |     "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.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.Title": "Load save", | ||||||
|     "Info.Hint.Load.File.Intro": "Release to load the dragged save file", |     "Info.Hint.Load.File.Intro": "Release to load the dragged save file", | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ const ZH_CN = { | |||||||
|     "Object.List.New.Group": "群对象 {id}", |     "Object.List.New.Group": "群对象 {id}", | ||||||
|     "Object.List.New.Range": "范围对象 {id}", |     "Object.List.New.Range": "范围对象 {id}", | ||||||
|     "Object.List.New.Label": "标签 {id}", |     "Object.List.New.Label": "标签 {id}", | ||||||
|  |     "Object.List.New.Clip": "剪辑片段 {id}", | ||||||
|     "Object.List.No.Data": "模型中没有任何对象,点击按钮以创建", |     "Object.List.No.Data": "模型中没有任何对象,点击按钮以创建", | ||||||
|     "Object.Picker.List.No.Data": "模型中没有合适此选项的模型", |     "Object.Picker.List.No.Data": "模型中没有合适此选项的模型", | ||||||
|     "Behavior.Picker.Add.Button": "点击此处以赋予行为到此群", |     "Behavior.Picker.Add.Button": "点击此处以赋予行为到此群", | ||||||
| @ -53,6 +54,11 @@ const ZH_CN = { | |||||||
|     "Panel.Info.Behavior.List.View": "编辑查看行为列表", |     "Panel.Info.Behavior.List.View": "编辑查看行为列表", | ||||||
|     "Panel.Title.Behavior.Details.View": "行为", |     "Panel.Title.Behavior.Details.View": "行为", | ||||||
|     "Panel.Info.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.Unnamed": "弹窗消息", | ||||||
|     "Popup.Title.Confirm": "确认消息", |     "Popup.Title.Confirm": "确认消息", | ||||||
|     "Popup.Action.Yes": "确定", |     "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.X": "{key} X 坐标", | ||||||
|     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y 坐标", |     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y 坐标", | ||||||
|     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标", |     "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标", | ||||||
|  |     "Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建", | ||||||
|     "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", |     "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", | ||||||
|     "Info.Hint.Load.File.Title": "加载存档", |     "Info.Hint.Load.File.Title": "加载存档", | ||||||
|     "Info.Hint.Load.File.Intro": "释放以加载拽入的存档", |     "Info.Hint.Load.File.Intro": "释放以加载拽入的存档", | ||||||
|  | |||||||
| @ -1,8 +1,17 @@ | |||||||
| import { Model } from "@Model/Model"; | import { Model } from "@Model/Model"; | ||||||
| import { Emitter } from "@Model/Emitter"; | import { Emitter } from "@Model/Emitter"; | ||||||
|  | import { Clip } from "@Model/Clip"; | ||||||
|  | 
 | ||||||
|  | enum ActuatorModel { | ||||||
|  | 	Play = 1, | ||||||
|  | 	Record = 2, | ||||||
|  | 	View = 3, | ||||||
|  | 	Offline = 4 | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| interface IActuatorEvent { | interface IActuatorEvent { | ||||||
| 	startChange: boolean; | 	startChange: boolean; | ||||||
|  | 	record: number; | ||||||
| 	loop: number; | 	loop: number; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -26,6 +35,47 @@ class Actuator extends Emitter<IActuatorEvent> { | |||||||
| 	 */ | 	 */ | ||||||
| 	private startFlag: boolean = false; | 	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<IActuatorEvent> { | |||||||
| 				} else { | 				} else { | ||||||
| 					this.alignTimer += durTime; | 					this.alignTimer += durTime; | ||||||
| 					if (this.alignTimer > (1 / this.fps)) { | 					if (this.alignTimer > (1 / this.fps)) { | ||||||
|  | 
 | ||||||
|  | 						// 更新模型
 | ||||||
| 						this.model.update(this.alignTimer * this.speed); | 						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.emit("loop", this.alignTimer); | ||||||
| 						this.alignTimer = 0; | 						this.alignTimer = 0; | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} | ||||||
|  | 		 | ||||||
|  | 		else { | ||||||
| 			this.emit("loop", Infinity); | 			this.emit("loop", Infinity); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @ -122,4 +189,4 @@ class Actuator extends Emitter<IActuatorEvent> { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export { Actuator } | export { Actuator, ActuatorModel } | ||||||
							
								
								
									
										105
									
								
								source/Model/Clip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								source/Model/Clip.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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 }; | ||||||
| @ -5,6 +5,7 @@ import { IParamValue } from "@Model/Parameter"; | |||||||
| import { CtrlObject } from "@Model/CtrlObject"; | import { CtrlObject } from "@Model/CtrlObject"; | ||||||
| import { Emitter } from "@Model/Emitter"; | import { Emitter } from "@Model/Emitter"; | ||||||
| import { AbstractRenderer } from "@Model/Renderer"; | import { AbstractRenderer } from "@Model/Renderer"; | ||||||
|  | import { Clip } from "@Model/Clip"; | ||||||
| import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "@Model/Behavior"; | import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "@Model/Behavior"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -22,6 +23,7 @@ type ModelEvent = { | |||||||
|     objectChange: CtrlObject[]; |     objectChange: CtrlObject[]; | ||||||
|     individualChange: Group; |     individualChange: Group; | ||||||
|     behaviorChange: IAnyBehavior; |     behaviorChange: IAnyBehavior; | ||||||
|  |     clipChange: Clip[]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
| @ -330,6 +332,51 @@ class Model extends Emitter<ModelEvent> { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * 剪辑数据 | ||||||
|  |      */ | ||||||
|  |     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<ModelEvent> { | |||||||
|                 object.runner(t, "finalEffect"); |                 object.runner(t, "finalEffect"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         this.draw(); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public draw() { |     public draw() { | ||||||
|  | |||||||
| @ -55,7 +55,7 @@ class SimulatorWeb extends Component { | |||||||
|                     items: [ |                     items: [ | ||||||
|                         {panels: ["RenderView"]}, |                         {panels: ["RenderView"]}, | ||||||
|                         { |                         { | ||||||
|                             items: [{panels: ["BehaviorList"]}, {panels: ["LabelList"]}], |                             items: [{panels: ["ClipPlayer", "BehaviorList"]}, {panels: ["LabelList"]}], | ||||||
|                             scale: 80, |                             scale: 80, | ||||||
|                             layout: LayoutDirection.X |                             layout: LayoutDirection.X | ||||||
|                         } |                         } | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								source/Panel/ClipPlayer/ClipPlayer.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								source/Panel/ClipPlayer/ClipPlayer.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | div.Clip-player-clip-list-root { | ||||||
|  | 	width: 100%; | ||||||
|  | 	height: 100%; | ||||||
|  | 	box-sizing: border-box; | ||||||
|  | 	padding: 10px; | ||||||
|  | } | ||||||
							
								
								
									
										42
									
								
								source/Panel/ClipPlayer/ClipPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								source/Panel/ClipPlayer/ClipPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<IMixinStatusProps> { | ||||||
|  | 
 | ||||||
|  | 	private renderMessage(): ReactNode { | ||||||
|  | 		return <Message i18nKey="Panel.Info.Clip.List.Error.Nodata"/>; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	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 <ClipList | ||||||
|  | 			clips={clipList} | ||||||
|  | 			disable={disable} | ||||||
|  | 		/>; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public render(): ReactNode { | ||||||
|  | 		const clipList = this.props.status?.model.clipPool ?? []; | ||||||
|  | 
 | ||||||
|  | 		return <Theme className="Clip-player-clip-list-root"> | ||||||
|  | 			{ clipList.length > 0 ? null : this.renderMessage() } | ||||||
|  | 			{ this.renderClipList(clipList) } | ||||||
|  | 		</Theme>; | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export { ClipPlayer }; | ||||||
							
								
								
									
										59
									
								
								source/Panel/ClipPlayer/ClipRecorder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								source/Panel/ClipPlayer/ClipRecorder.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -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<IMixinStatusProps> { | ||||||
|  | 	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 <Recorder | ||||||
|  | 			currentTime={currentTime} | ||||||
|  | 			mode={mod} | ||||||
|  | 			running={runner} | ||||||
|  | 			action={() => { | ||||||
|  | 
 | ||||||
|  | 				// 开启录制
 | ||||||
|  | 				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 }; | ||||||
| @ -10,6 +10,8 @@ import { LabelDetails } from "@Panel/LabelDetails/LabelDetails"; | |||||||
| import { GroupDetails } from "@Panel/GroupDetails/GroupDetails"; | import { GroupDetails } from "@Panel/GroupDetails/GroupDetails"; | ||||||
| import { BehaviorList } from "@Panel/BehaviorList/BehaviorList"; | import { BehaviorList } from "@Panel/BehaviorList/BehaviorList"; | ||||||
| import { BehaviorDetails } from "@Panel/BehaviorDetails/BehaviorDetails"; | import { BehaviorDetails } from "@Panel/BehaviorDetails/BehaviorDetails"; | ||||||
|  | import { ClipPlayer } from "@Panel/ClipPlayer/ClipPlayer"; | ||||||
|  | import { ClipRecorder } from "@Panel/ClipPlayer/ClipRecorder"; | ||||||
| 
 | 
 | ||||||
| interface IPanelInfo { | interface IPanelInfo { | ||||||
| 	nameKey: string; | 	nameKey: string; | ||||||
| @ -31,6 +33,7 @@ type PanelId = "" | |||||||
| | "GroupDetails" // 群属性
 | | "GroupDetails" // 群属性
 | ||||||
| | "BehaviorList" // 行为列表
 | | "BehaviorList" // 行为列表
 | ||||||
| | "BehaviorDetails" // 行为属性
 | | "BehaviorDetails" // 行为属性
 | ||||||
|  | | "ClipPlayer" // 剪辑影片
 | ||||||
| ; | ; | ||||||
| 
 | 
 | ||||||
| const PanelInfoMap = new Map<PanelId, IPanelInfo>(); | const PanelInfoMap = new Map<PanelId, IPanelInfo>(); | ||||||
| @ -66,6 +69,10 @@ PanelInfoMap.set("BehaviorDetails", { | |||||||
|     nameKey: "Panel.Title.Behavior.Details.View", introKay: "Panel.Info.Behavior.Details.View", |     nameKey: "Panel.Title.Behavior.Details.View", introKay: "Panel.Info.Behavior.Details.View", | ||||||
|     class: BehaviorDetails |     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 { | function getPanelById(panelId: PanelId): ReactNode { | ||||||
| 	switch (panelId) { | 	switch (panelId) { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user