aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json1
-rw-r--r--pages/editor.tsx97
-rw-r--r--pages/present.tsx8
-rw-r--r--styles/editor.css46
-rw-r--r--styles/globals.css15
-rw-r--r--yarn.lock5
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;
+}
+
diff --git a/yarn.lock b/yarn.lock
index adc2d5a..df0ae64 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"