diff options
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | pages/editor.tsx | 97 | ||||
| -rw-r--r-- | pages/present.tsx | 8 | ||||
| -rw-r--r-- | styles/editor.css | 46 | ||||
| -rw-r--r-- | styles/globals.css | 15 | ||||
| -rw-r--r-- | yarn.lock | 5 | 
6 files changed, 168 insertions, 4 deletions
| diff --git a/package.json b/package.json index bb41537..91bdf2f 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@  		"next": "^10.2.0",  		"react": "^17.0.2",  		"react-dom": "^17.0.2", +		"timecode-boss": "^4.2.3",  		"ts-json-schema-generator": "^0.92.0"  	},  	"devDependencies": { diff --git a/pages/editor.tsx b/pages/editor.tsx index c550eac..aeb4f87 100644 --- a/pages/editor.tsx +++ b/pages/editor.tsx @@ -1,8 +1,13 @@ +import { useEffect, useState } from 'react'; +import { loopSlide } from '../timeline'; +import { TimedVideoPlayer } from './present'; +  import AppBar from '@material-ui/core/AppBar'; +import Button from '@material-ui/core/Button';  import Fab from '@material-ui/core/Fab';  import Toolbar from '@material-ui/core/Toolbar'; -import { PressureIcon } from '../components/icons'; +import { PressureIcon, SlideKeyframe } from '../components/icons';  import FullscreenRoundedIcon from '@material-ui/icons/FullscreenRounded';  import NavigateBeforeRoundedIcon from '@material-ui/icons/NavigateBeforeRounded'; @@ -10,7 +15,44 @@ import NavigateNextRoundedIcon from '@material-ui/icons/NavigateNextRounded';  import PauseRoundedIcon from '@material-ui/icons/PauseRounded';  import SkipPreviousRoundedIcon from '@material-ui/icons/SkipPreviousRounded'; +function TimelineEditor(props: { +	player: TimedVideoPlayer; +}) { +	var frames = [...new Array(props.player.timeline?.framecount || 0)].map((el, i) => +		<div className='frame'> +			<span className='timecode numbers posabs abscenterh'> +				{props.player?.frameToTimestampString(i + 1)} +			</span> +			<div className='keyframeWrapper posabs abscenterh'> +				{(() => { +					var slide = props.player?.timeline?.slides.find(slide => slide.frame == i + 1); +					if (slide) { +						return <SlideKeyframe type={slide.type} loopEnd />; +					} +					var loop = props.player?.timeline?.slides.find(slide => +						slide.type == 'loop' && (slide as loopSlide).beginFrame == i + 1 +					); +					if (loop) { +						return <SlideKeyframe type='loop' />; +					} +				})()} +			</div> +		</div> +	); + +	return <div className='frames'>{frames}</div>; +} +  export default function Index() { +	var [dummy, setDummy] = useState(false); +	var rerender = () => setDummy(!dummy); +	var [player, setPlayer] = useState(new TimedVideoPlayer()); + +	useEffect(() => { +		var videoEL = document.getElementById('player') as HTMLVideoElement; +		player.registerPlayer(videoEL); +	}, []); +  	return <>  		<div className='appGrid posabs a0'>  			<AppBar position='static' color='transparent' elevation={0}> @@ -19,10 +61,55 @@ export default function Index() {  					<h1>pressure</h1>  				</Toolbar>  			</AppBar> -			<div className='settings'></div> +			<div className='settings'> +				<input +					type='file' +					id='vidUpload' +					accept='video/*' +					className='dispnone' +					onChange={event => { +						var file = event.target.files[0]; +						if (!file) return; +						var reader = new FileReader(); +						reader.addEventListener('load', ev => { +							player.loadVideo(ev.target.result as string); +						}); +						reader.readAsDataURL(file); +					}} +				/> +				<input +					type='file' +					id='jsonUpload' +					accept='application/json' +					className='dispnone' +					onChange={event => { +						var file = event.target.files[0]; +						if (!file) return; +						var reader = new FileReader(); +						reader.addEventListener('load', ev => { +							player.loadSlides(ev.target.result as string); +							rerender(); +						}); +						reader.readAsText(file); +					}} +				/> +				<Button +					variant='contained' +					color='default' +					children='Load video' +					onClick={() => document.getElementById('vidUpload').click()} +				/> +				<Button +					variant='contained' +					color='default' +					children='Load json' +					onClick={() => document.getElementById('jsonUpload').click()} +				/> +			</div>  			<div className='viewer'>  				<div className='player posrel'>  					<div className='outer posabs abscenter'> +						<video id='player' className='fullwidth' />  					</div>  				</div>  				<div className='controls'> @@ -38,7 +125,11 @@ export default function Index() {  				</div>  			</div>  			<div className='tools'></div> -			<div className='timeline'></div> +			<div className='timeline'> +				<TimelineEditor +					player={player} +				/> +			</div>  		</div>  	</>;  } diff --git a/pages/present.tsx b/pages/present.tsx index ad3cec2..19ef111 100644 --- a/pages/present.tsx +++ b/pages/present.tsx @@ -1,6 +1,7 @@  import Button from '@material-ui/core/Button';  import Ajv from 'ajv';  import { useEffect, useState } from 'react'; +import Timecode from 'timecode-boss';  import timeline, { delaySlide, loopSlide, slide, speedChangeSlide } from '../timeline';  import * as timelineSchema from '../timeline.schema.json'; @@ -12,7 +13,7 @@ import SettingsRoundedIcon from '@material-ui/icons/SettingsRounded';  import CodeRoundedIcon from '@material-ui/icons/CodeRounded';  import MovieRoundedIcon from '@material-ui/icons/MovieRounded'; -class TimedVideoPlayer { +export class TimedVideoPlayer {  	slide: number;  	timeline: timeline;  	precision: number; @@ -30,6 +31,11 @@ class TimedVideoPlayer {  		this.registeredEventListeners = false;  	} +	frameToTimestampString(frame: number) { +		var timecodeString = new Timecode(frame, this.framerate).toString(); +		return timecodeString.replace(/^(00:)+/, '') + 'f'; +	} +  	timestampToFrame(timestamp: number): number {  		return Math.ceil((timestamp * 1e3) / (1e3 / this.framerate));  	} diff --git a/styles/editor.css b/styles/editor.css index a25108d..8848b6a 100644 --- a/styles/editor.css +++ b/styles/editor.css @@ -48,6 +48,7 @@  .appGrid .viewer .player {  	margin: 16px;  	margin-bottom: 0px; +	line-height: 0;  }  .appGrid .viewer .player .outer { @@ -92,3 +93,48 @@  	box-shadow: none;  	margin: 4px;  } + +.appGrid .timeline .frames { +	height: 100%; +} + +.appGrid .timeline .frames .frame { +	background-color: transparent; +	margin-top: 28px; +	height: 100%; +	width: 4px; +	display: inline-block; +	border-radius: 2px 2px 0 0; +	position: relative; +	overflow: visible; +} + +.appGrid .timeline .frames .frame .timecode { +	display: none; +} + +.appGrid .timeline .frames .frame .keyframeWrapper { +	line-height: 0; +	top: 16px; +	z-index: 999; + +} + +.appGrid .timeline .frames .frame:nth-child(30n) { +	background-color: var(--c300); +} + +.appGrid .timeline .frames .frame:nth-child(30n) .timecode { +	color: var(--c700); +	font-weight: 500; +	line-height: 1; +	display: inline-block; +	top: -16px; +} + +.appGrid .timeline { +	overflow-y: hidden; +	overflow-x: scroll; +	white-space: nowrap; +} + diff --git a/styles/globals.css b/styles/globals.css index 9ed6119..c45aa2d 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -23,3 +23,18 @@ h3 { font-size: 1rem; }  	font-family: "Inter", sans-serif;  } +::-webkit-scrollbar-track, +::-webkit-scrollbar-track-piece, +::-webkit-scrollbar { +	background-color: var(--c100); +} + +::-webkit-scrollbar-thumb { +	background-color: var(--c400); +} + +::-webkit-scrollbar { +	width: 8px; +	height: 8px; +} + @@ -2055,6 +2055,11 @@ symbol-observable@1.2.0:    resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"    integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +timecode-boss@^4.2.3: +  version "4.2.3" +  resolved "https://registry.yarnpkg.com/timecode-boss/-/timecode-boss-4.2.3.tgz#c569ef8b815c0434ad551f7681577cd46949abf9" +  integrity sha512-DnZWYwn/QCAW3ElK0TOaAy3v2QEr6I+VMdc1aqo/9/H0uiebMp5xIeaU44IDpwQUE7W6W4k2+2aDM6VwQaXjOQ== +  timers-browserify@2.0.12, timers-browserify@^2.0.4:    version "2.0.12"    resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" |