├── .github └── .keep ├── fire ├── src │ ├── App.css │ ├── react-app-env.d.ts │ ├── routes │ │ ├── about.module.css │ │ ├── img │ │ │ └── background.png │ │ ├── editor.module.css │ │ ├── about.tsx │ │ ├── login.module.css │ │ ├── login.tsx │ │ ├── exportPage.tsx │ │ ├── exportPage.module.css │ │ ├── projects.module.css │ │ ├── editor.tsx │ │ └── projects.tsx │ ├── App.tsx │ ├── setupTests.js │ ├── App.test.js │ ├── components │ │ ├── mediaPlayer │ │ │ ├── mediaPlayer.module.css │ │ │ └── mediaPlayer.tsx │ │ ├── actions │ │ │ ├── actions.module.css │ │ │ └── actions.tsx │ │ ├── controls │ │ │ ├── controls.module.css │ │ │ └── controls.tsx │ │ ├── mediaPool │ │ │ ├── mediaPool.module.css │ │ │ └── mediaPool.tsx │ │ ├── timeline │ │ │ ├── timeline.module.css │ │ │ └── timeline.tsx │ │ └── elements │ │ │ └── properties.module.css │ ├── reportWebVitals.js │ ├── index.js │ ├── model │ │ ├── shaders.ts │ │ ├── types.ts │ │ ├── projectManager.tsx │ │ ├── playbackController.tsx │ │ ├── webgl.ts │ │ └── mediaManager.tsx │ ├── utils │ │ └── utils.ts │ ├── index.css │ └── logo.svg ├── public │ ├── robots.txt │ ├── logo192.png │ ├── logo512.png │ ├── google.svg │ ├── manifest.json │ └── index.html ├── .gitignore ├── tsconfig.json ├── package.json └── README.md ├── .DS_Store ├── doc ├── .DS_Store ├── sprint0 │ ├── ux.pdf │ ├── .DS_Store │ ├── personas.pdf │ ├── team-contract.pdf │ ├── sprint0_marking-thenerds.txt │ ├── done.md │ ├── process.md │ ├── team.md │ ├── competition.md │ ├── summary.md │ └── PB.md ├── sprint2 │ ├── burndown.pdf │ ├── schedule.pdf │ ├── System Design Document.pdf │ ├── SR2.md │ ├── sprint2.md │ ├── PB.md │ └── sprint2-thenerds.txt ├── sprint3 │ ├── burndown.pdf │ ├── schedule.pdf │ ├── System Design Document.pdf │ ├── Sprint3Demo_thenerds.txt │ ├── SR3.md │ ├── sprint3.md │ ├── PB.md │ └── sprint3-marks.txt ├── sprint4 │ ├── burndown.pdf │ ├── schedule.pdf │ ├── System Design Document.pdf │ ├── SR4.md │ ├── PB.md │ ├── sprint4.md │ └── sprint4-thenerds.txt ├── sprint1 │ ├── System Design Document.pdf │ ├── RPM.md │ ├── SR1.md │ ├── sprint1.md │ └── Sprint1_marking.txt ├── sprint4_demo.txt ├── sprint2_demo.txt └── sprint1_demo.txt ├── README.md ├── backend ├── .gitignore ├── package.json └── server.js └── presentation.md /.github/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fire/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fire/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/.DS_Store -------------------------------------------------------------------------------- /doc/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/.DS_Store -------------------------------------------------------------------------------- /fire/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /doc/sprint0/ux.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint0/ux.pdf -------------------------------------------------------------------------------- /doc/sprint0/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint0/.DS_Store -------------------------------------------------------------------------------- /doc/sprint0/personas.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint0/personas.pdf -------------------------------------------------------------------------------- /doc/sprint2/burndown.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint2/burndown.pdf -------------------------------------------------------------------------------- /doc/sprint2/schedule.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint2/schedule.pdf -------------------------------------------------------------------------------- /doc/sprint3/burndown.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint3/burndown.pdf -------------------------------------------------------------------------------- /doc/sprint3/schedule.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint3/schedule.pdf -------------------------------------------------------------------------------- /doc/sprint4/burndown.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint4/burndown.pdf -------------------------------------------------------------------------------- /doc/sprint4/schedule.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint4/schedule.pdf -------------------------------------------------------------------------------- /fire/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/fire/public/logo192.png -------------------------------------------------------------------------------- /fire/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/fire/public/logo512.png -------------------------------------------------------------------------------- /doc/sprint0/team-contract.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint0/team-contract.pdf -------------------------------------------------------------------------------- /fire/src/routes/about.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 25px; 3 | font-size: large; 4 | color: var(--font-color); 5 | } 6 | -------------------------------------------------------------------------------- /fire/src/routes/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/fire/src/routes/img/background.png -------------------------------------------------------------------------------- /doc/sprint1/System Design Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint1/System Design Document.pdf -------------------------------------------------------------------------------- /doc/sprint2/System Design Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint2/System Design Document.pdf -------------------------------------------------------------------------------- /doc/sprint3/System Design Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint3/System Design Document.pdf -------------------------------------------------------------------------------- /doc/sprint4/System Design Document.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint4/System Design Document.pdf -------------------------------------------------------------------------------- /doc/sprint0/sprint0_marking-thenerds.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RohanPoojary1107/fire-video-editor/HEAD/doc/sprint0/sprint0_marking-thenerds.txt -------------------------------------------------------------------------------- /fire/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import ProjectManager from "./model/projectManager"; 3 | 4 | function App() { 5 | return ( 6 | 7 | ); 8 | } 9 | 10 | export default App; 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fire video editor :fire: 2 | A simple video editing software on the web. 3 | 4 | 5 | Screen Shot 2021-10-24 at 7 09 40 PM-min 6 | -------------------------------------------------------------------------------- /fire/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /fire/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /doc/sprint0/done.md: -------------------------------------------------------------------------------- 1 | The definition of “done” is as follows:
2 | A user story is considered to be done when the 4 checkpoints below are completed” 3 | - Testing written and passing 4 | - Code is peer reviewed 5 | - Code is documented 6 | - Acceptance criteria met (Criteria of Satisfaction) 7 | -------------------------------------------------------------------------------- /fire/src/components/mediaPlayer/mediaPlayer.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: mediaPlayer; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | padding: 15px 15px 5px 15px; 7 | 8 | } 9 | 10 | .canvas { 11 | background-color: black; 12 | max-width: 100%; 13 | max-height: 100%; 14 | } 15 | -------------------------------------------------------------------------------- /fire/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | future.txt -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "cors": "^2.8.5", 13 | "express": "^4.17.1", 14 | "google-auth-library": "^7.0.2", 15 | "mongodb": "^3.6.5", 16 | "uuid": "^8.3.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /fire/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .eslintcache 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | future.txt -------------------------------------------------------------------------------- /fire/public/google.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/sprint4_demo.txt: -------------------------------------------------------------------------------- 1 | 2 | Marking Rubric for the Sprint 4 class demo 3 | ========================================== 4 | Team name: The-Nerds 5 | 6 | Attendance: 2 marks full team is attending, 1 mark if at least one member not present, 0 marks the whole team not present 7 | Your mark: 2 8 | 9 | Working software: 8 marks if at least 4 features work flawlessly, take away 1 mark for each error or missing records on Jira. 10 | Your mark: 8 11 | 12 | Note: Great work nerds!! 13 | 14 | Total demo mark: 10/10 15 | -------------------------------------------------------------------------------- /doc/sprint3/Sprint3Demo_thenerds.txt: -------------------------------------------------------------------------------- 1 | Marking Rubric for the Sprint 1 class demo 2 | ========================================== 3 | Team name: The Nerds 4 | 5 | Attendance: 2 marks full team is attending, 1 mark if at least one member not present, 0 marks the whole team not present 6 | Your mark: 2 7 | 8 | 9 | Working software: 8 marks if features work flawlessly as described in Jira, take away 1 mark for each problem you spot until the mark hits zero. 10 | Your mark: 8 11 | 12 | 13 | Total demo mark: 10/10 14 | -------------------------------------------------------------------------------- /doc/sprint2_demo.txt: -------------------------------------------------------------------------------- 1 | Marking Rubric for the Sprint 2 class demo 2 | ========================================== 3 | Team name: The Nerds 4 | 5 | Attendance: 2 marks full team is attending, 1 mark if at least one member not present, 0 marks the whole team not present 6 | Your mark: 2 7 | 8 | Working software: 8 marks if at least 4 features work flawlessly, take away 1 mark for each error or missing records on Jira. 9 | Your mark: 8 10 | 11 | Notes: Keep up the great work!! I'm always impressed each demo :) 12 | 13 | Total demo mark: 10/10 14 | 15 | -------------------------------------------------------------------------------- /doc/sprint1_demo.txt: -------------------------------------------------------------------------------- 1 | Marking Rubric for the Sprint 1 class demo 2 | ========================================== 3 | Team name: The Nerds 4 | 5 | Attendance: 2 marks full team is attending, 1 mark if at least one member not present, 0 marks the whole team not present 6 | Your mark: 2 7 | 8 | Working software: 8 marks if at least 4 features work flawlessly, take away 1 mark for each error or missing records on Jira. 9 | Your mark: 8 10 | 11 | Comments: Really loved your application's design!! Keep up the great work. 12 | 13 | Total demo mark: 10/10 14 | -------------------------------------------------------------------------------- /fire/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /fire/src/model/shaders.ts: -------------------------------------------------------------------------------- 1 | export let VERTEX_SHADER = ` 2 | attribute vec4 a_position; 3 | attribute vec2 a_texcoord; 4 | 5 | uniform mat4 u_matrix; 6 | 7 | varying vec2 v_texcoord; 8 | 9 | void main() { 10 | gl_Position = u_matrix * a_position; 11 | v_texcoord = a_texcoord; 12 | } 13 | `; 14 | 15 | export let FRAGMENT_SHADER = ` 16 | precision mediump float; 17 | 18 | uniform sampler2D u_texture; 19 | 20 | varying vec2 v_texcoord; 21 | 22 | void main() { 23 | gl_FragColor = texture2D(u_texture, v_texcoord); 24 | } 25 | `; -------------------------------------------------------------------------------- /fire/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /fire/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /doc/sprint1/RPM.md: -------------------------------------------------------------------------------- 1 | # Release Goal 2 | 3 | Deploy the fully featured fire editor to the cloud. 4 | 5 | # Scope of Project 6 | 7 | - Have a user-friendly UI. 8 | - Be able to manage and play videos. 9 | - Be able to split, delete, move, trim, scale, crop and position videos. 10 | - Be able to create transitions and animations. 11 | - Be able to login and save projects to the cloud. 12 | - Be able to load projects from the cloud. 13 | 14 | # Participants 15 | 16 | - Rohan Poojary (poojaryr) 17 | - Julian de Rushe (derushej) 18 | - Mohammad Tahvili (tahvilim) 19 | - Alees Ahmad Goraya (gorayaal) 20 | - Tomasz Cieslak (cieslak4) 21 | - Jagdev Singh Jhajj (jhajjjag) 22 | - Bhavik Kothari (kothar33) 23 | -------------------------------------------------------------------------------- /fire/src/components/mediaPlayer/mediaPlayer.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./mediaPlayer.module.css"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | export default function MediaPlayer({ 5 | canvasRef, 6 | projectWidth, 7 | projectHeight, 8 | }: { 9 | canvasRef: HTMLCanvasElement; 10 | projectWidth: number; 11 | projectHeight: number; 12 | }) { 13 | const ref = useRef(null); 14 | 15 | useEffect(() => { 16 | canvasRef.classList.add(styles.canvas); 17 | //@ts-ignore 18 | canvasRef.style.aspectRatio = `${projectWidth / projectHeight}`; 19 | ref.current?.appendChild(canvasRef); 20 | }, []); 21 | 22 | return
; 23 | } 24 | -------------------------------------------------------------------------------- /doc/sprint0/process.md: -------------------------------------------------------------------------------- 1 |

process.md


2 | - How did you organize the team? Which tools did you use, if any?
3 | We created a Discord server to communicate with each other.
4 | - How did you make decisions?
5 | Our decisions were made by reaching a consensus.
6 | - How did you define priority and/or points of user stories? How many rounds of voting (on average) did you need to come to a consensus for the point estimate?
7 | Points for the user stories were based on the difficulty, and size of the user story. Priority was defined based on the degradation of usability and user experience.
8 | - How frequently did you meet?
9 | We met 3 times a week for 1 hour each.
10 | - What lessons should you take forward to the next phase?
11 | We should start working on the deliverables earlier and get more organized using JIRA. 12 | -------------------------------------------------------------------------------- /fire/src/routes/editor.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: grid; 3 | 4 | grid-template-areas: 5 | "mediaPool mediaPlayer elements elements actions" 6 | "mediaPool controls controls controls controls" 7 | "mediaPool timeline timeline timeline timeline"; 8 | 9 | grid-template-columns: 300px minmax(0, 1fr) min-content; 10 | grid-template-rows: minmax(0, 1fr) min-content 300px; 11 | 12 | height: 100vh; 13 | } 14 | 15 | @media (max-width: 1100px) { 16 | .container { 17 | grid-template-areas: 18 | "mediaPool" 19 | "elements" 20 | "actions" 21 | "mediaPlayer" 22 | "controls" 23 | "timeline"; 24 | 25 | grid-template-columns: 1fr; 26 | grid-template-rows: 27 | min-content 28 | min-content 29 | min-content 30 | min-content 31 | min-content; 32 | height: auto; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /fire/src/routes/about.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import styles from "./about.module.css"; 3 | 4 | export default function About() { 5 | return ( 6 |
7 | arrow_back 8 |

About Us

9 |

We are a group of developers passionate to bring the best products to consumers.

10 |

We are constantly testing our products for bugs and issues

11 |

If you come accross any such bug or just want to give feedback to our team don't hesisate to contact us

12 |

13 |
14 |

Email Address: react.editor@gmail.com

15 |
16 | 17 |

Version 0.1

18 |
); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /fire/src/components/actions/actions.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: actions; 3 | display: flex; 4 | flex-direction: column; 5 | padding: var(--spacing); 6 | padding-left: 0; 7 | gap: 10px; 8 | position: absolute; 9 | right: 0px; 10 | } 11 | 12 | .logo { 13 | width: calc(100% - 10px); 14 | } 15 | 16 | .popup { 17 | position: fixed;bottom:30px;right:30px;width:100vw;text-align:right; 18 | } 19 | 20 | .popupContainer{width:100%;max-width:300px;padding:25px 15px;border-radius:5px;float:right;color:#fff;text-align:left;} 21 | .popupClose{background:transparent;width:20px;height:20px;margin:0px;position:absolute;top:5px;right:10px;} 22 | .popupClose:hover{background:transparent;} 23 | .popupClose span{color:#fff;font-size:18px;} 24 | 25 | @media (max-width: 1100px) { 26 | .container { 27 | position: relative; 28 | flex-direction: row; 29 | padding-left: var(--spacing); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /doc/sprint1/SR1.md: -------------------------------------------------------------------------------- 1 | # Unfinished Tasks 2 | 3 | There are no unfinished tasks. 4 | 5 | # Practices to Continue 6 | 7 | - Consistent meetings with all group members. 8 | - Handling bottleneck issues ASAP. 9 | - Involving the team and updating JIRA when issues arise. 10 | 11 | # New Practices to Follow 12 | 13 | - Pair up with a team member to code review your merges for the rest of the project. 14 | 15 | # Harmful Practices to Discard 16 | 17 | - Working on tasks longer than necessary. 18 | - Not splitting up features with enough subtasks. 19 | 20 | # Sprint Experience 21 | 22 | - Collaborating as a team in a positive environment was helpful. 23 | - Waiting for members to complete their tasks might slow down development. 24 | 25 | # Participants 26 | 27 | - Rohan Poojary (poojaryr) 28 | - Julian de Rushe (derushej) 29 | - Mohammad Tahvili (tahvilim) 30 | - Alees Ahmad Goraya (gorayaal) 31 | - Tomasz Cieslak (cieslak4) 32 | - Jagdev Singh Jhajj (jhajjjag) 33 | - Bhavik Kothari (kothar33) 34 | -------------------------------------------------------------------------------- /doc/sprint4/SR4.md: -------------------------------------------------------------------------------- 1 | # Unfinished Tasks 2 | 3 | THEN-38 4 | 5 | # Practices to Continue 6 | 7 | - Consistent meetings with all group members and delivering continuous feedback. 8 | - Handling bottleneck issues ASAP. 9 | - Involving the team and updating JIRA when issues arise. 10 | 11 | # New Practices to Follow 12 | 13 | - Following the priciple of KISS - (Keep It Simple Stupid) to write simpler, maintainable code. 14 | 15 | # Harmful Practices to Discard 16 | 17 | - Working on tasks longer than necessary. 18 | - Cramming work during the end of the sprint. 19 | 20 | # Sprint Experience 21 | 22 | - Getting started early helped us address all issues in time. 23 | - Collaborating as a team in a positive environment was helpful. 24 | - Spacing out standups helped us stay connected. 25 | 26 | # Participants 27 | 28 | - Rohan Poojary (poojaryr) 29 | - Julian de Rushe (derushej) 30 | - Mohammad Tahvili (tahvilim) 31 | - Alees Ahmad Goraya (gorayaal) 32 | - Tomasz Cieslak (cieslak4) 33 | - Jagdev Singh Jhajj (jhajjjag) 34 | - Bhavik Kothari (kothar33) 35 | -------------------------------------------------------------------------------- /doc/sprint0/team.md: -------------------------------------------------------------------------------- 1 | | Full Name | UTORID | Student ID | Email (utoronto only) | Best way to contact | 2 | | --- | --- | --- | --- | --- | 3 | | Julian de Rushe | derushej | 1005458700 | julian.derushe@mail.utoronto.ca | Discord (ANinja_Cow#4683) | 4 | | Mohammad Tahvili | tahvilim | 1005308926 | mohammad.tahvili@mail.utoronto.ca | Discord (Bluberino#1666) | 5 | | Jagdev Singh Jhajj | jhajjjag | 1003744843 | jagdev.jhajj@mail.utoronto.ca | Discord (Jagdev#6646) | 6 | | Bhavik Kothari | kothar33 | 1005094719 | bhavik.kothari@mail.utoronto.ca | Discord (bhavik#1667) | 7 | | Alees Ahmad Goraya | gorayaal | 1004717808 | alees.goraya@mail.utoronto.ca | Discord (antineutrino#2772) | 8 | | Rohan Poojary | poojaryr | 1004743398 | rohan.poojary@mail.utoronto.ca | Discord (Rohan1107#5861) | 9 | | Tomasz Cieslak | cieslak4 | 1005361948 | t.cieslak@mail.utoronto.ca | Discord (Tomasz#2581) | 10 | 11 | https://discord.gg/PvvRRVZnrh 12 | -------------------------------------------------------------------------------- /doc/sprint2/SR2.md: -------------------------------------------------------------------------------- 1 | # Unfinished Tasks 2 | 3 | There are no unfinished tasks. 4 | 5 | # Practices to Continue 6 | 7 | - Consistent meetings with all group members and delivering continuous feedback. 8 | - Handling bottleneck issues ASAP. 9 | - Involving the team and updating JIRA when issues arise. 10 | 11 | # New Practices to Follow 12 | 13 | - Pair up with a team member to code review your merges for the rest of the project. 14 | 15 | # Harmful Practices to Discard 16 | 17 | - Working on tasks longer than necessary. 18 | - Not splitting up features with enough subtasks. 19 | - Cramming work during the end of the sprint. 20 | 21 | # Sprint Experience 22 | 23 | - Sprint2 went much smoother than Sprint1. 24 | - Collaborating as a team in a positive environment was helpful. 25 | - Waiting for members to complete their tasks might slow down development. 26 | 27 | # Participants 28 | 29 | - Rohan Poojary (poojaryr) 30 | - Julian de Rushe (derushej) 31 | - Mohammad Tahvili (tahvilim) 32 | - Alees Ahmad Goraya (gorayaal) 33 | - Tomasz Cieslak (cieslak4) 34 | - Jagdev Singh Jhajj (jhajjjag) 35 | - Bhavik Kothari (kothar33) 36 | -------------------------------------------------------------------------------- /doc/sprint3/SR3.md: -------------------------------------------------------------------------------- 1 | # Unfinished Tasks 2 | 3 | There are no unfinished tasks. 4 | 5 | # Practices to Continue 6 | 7 | - Consistent meetings with all group members and delivering continuous feedback. 8 | - Handling bottleneck issues ASAP. 9 | - Involving the team and updating JIRA when issues arise. 10 | 11 | # New Practices to Follow 12 | 13 | - Following the priciple of DRY - (Don't Repeat Yourself) to write simpler and cleaner code. 14 | 15 | # Harmful Practices to Discard 16 | 17 | - Working on tasks longer than necessary. 18 | - Not splitting up features with enough subtasks. 19 | - Cramming work during the end of the sprint. 20 | 21 | # Sprint Experience 22 | 23 | - Getting started early helped us address all issues in time. 24 | - Collaborating as a team in a positive environment was helpful. 25 | - Spacing out standups helped us stay connected. 26 | 27 | # Participants 28 | 29 | - Rohan Poojary (poojaryr) 30 | - Julian de Rushe (derushej) 31 | - Mohammad Tahvili (tahvilim) 32 | - Alees Ahmad Goraya (gorayaal) 33 | - Tomasz Cieslak (cieslak4) 34 | - Jagdev Singh Jhajj (jhajjjag) 35 | - Bhavik Kothari (kothar33) 36 | -------------------------------------------------------------------------------- /fire/src/model/types.ts: -------------------------------------------------------------------------------- 1 | export interface Source { 2 | track: number; 3 | element: HTMLVideoElement; 4 | inUse: boolean; 5 | } 6 | export interface Media { 7 | sources: Source[]; // Source 0 should allways be present 8 | file: File; 9 | thumbnail: string; 10 | } 11 | 12 | export interface Segment { 13 | media: Media; 14 | start: number; // Global start 15 | duration: number; 16 | mediaStart: number; 17 | keyframes: KeyFrame[]; // Keyframe 0 should allways be present 18 | texture: WebGLTexture; 19 | } 20 | 21 | export interface KeyFrame { 22 | start: number; // Offset from segment start 23 | x?: number; 24 | y?: number; 25 | scaleX?: number; 26 | scaleY?: number; 27 | trimLeft?: number; 28 | trimRight?: number 29 | trimTop?: number; 30 | trimBottom?: number; 31 | } 32 | 33 | export interface SegmentID { 34 | index: number; 35 | track: number; 36 | } 37 | 38 | 39 | export interface Project { 40 | _id: string; 41 | name: string; 42 | width: number; 43 | height: number; 44 | framerate: number; 45 | duration: number; 46 | } -------------------------------------------------------------------------------- /fire/src/routes/login.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | color: var(--font-color); 3 | background-color: var(--timeline-background-color); 4 | min-height:100vh; 5 | width:100vw; 6 | } 7 | 8 | .logo { 9 | background: var(--media-pool-background-color); 10 | border-radius:200px; 11 | padding:15px; 12 | max-width: 60px; 13 | margin-bottom:30px; 14 | } 15 | 16 | .button{ 17 | background: var(--media-pool-background-color); 18 | border-radius:25px; 19 | padding:25px 10px; 20 | color: var(--font-color); 21 | display:flex; 22 | width:100%; 23 | align-items: center; 24 | } 25 | 26 | .loginBox{width:calc(100% - 60px);display:block;max-width:360px;padding:30px;margin:auto;} 27 | 28 | .loginBox a{display:block;text-align:center;} 29 | 30 | .google{ 31 | width:30px;margin-right:10px; 32 | } 33 | 34 | @media (min-width: 1100px) { 35 | .container { 36 | display:grid; 37 | grid-template-columns: 1fr 1fr; 38 | background-image: url('./img/background.png'); 39 | background-position: left top; 40 | background-size: cover; 41 | background-repeat: no-repeat; 42 | } 43 | .login{ 44 | display:flex; 45 | align-items: center; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /doc/sprint0/competition.md: -------------------------------------------------------------------------------- 1 | Our video editor provides users with an easy tool to manage and edit their videos online without downloading a large application. This type of editor is not the first of its kind but will be a unique product that will stand out from the handful of competitors. Identifying the competition is crucial for our success since it allows our product to be distinguishable by introducing features that the competition lacks. 2 | 3 | The online video editor [Clipchamp](https://clipchamp.com/en/video-editor/), developed in 2012 is fully functional and is a direct competitor to our developing product. Our editing platform is different by providing custom animations through the use of keyframes. If a beginner editor is looking to flair up the animation side of their edit, our product will allow an image to have a beginning state or condition that can change over time to an end state (i.e. an animation), rather than our competitor’s limited static image placement. 4 | 5 | The competition did not focus on the keyframe aspect of the editor most likely because they expect their user-base to not be experienced enough to use that specific feature. Allowing keyframing involves setting attributes of position and time and interpolating between them to insert images onto a video. This feature allows for the possibility for animation and transition. 6 | -------------------------------------------------------------------------------- /fire/src/components/controls/controls.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: controls; 3 | display: flex; 4 | flex-direction: row; 5 | padding: var(--spacing); 6 | align-items: center; 7 | background: transparent; 8 | gap: var(--spacing); 9 | z-index: 4; 10 | } 11 | 12 | .trackbar { 13 | flex: 1; 14 | -webkit-appearance: none; 15 | height: 5px; 16 | border-radius: var(--border-radius); 17 | background: var(--black-color); 18 | outline: none; 19 | transition: opacity 0.2s; 20 | } 21 | 22 | .trackbar:hover { 23 | background: var(--icon-background-hover-color); 24 | cursor: pointer; 25 | } 26 | 27 | .trackbar:focus { 28 | box-shadow: 0px 0px 5px 0px #cacaca; 29 | } 30 | 31 | .trackbar::-webkit-slider-thumb { 32 | -webkit-appearance: none; 33 | appearance: none; 34 | width: 20px; 35 | height: 20px; 36 | border-radius: 50%; 37 | background: var(--font-color); 38 | } 39 | 40 | .trackbar::-moz-range-thumb { 41 | width: 20px; 42 | height: 20px; 43 | border-radius: 50%; 44 | background: var(--font-color); 45 | } 46 | 47 | @media (max-width: 1100px) { 48 | .container { 49 | grid-area: controls; 50 | display: flex; 51 | flex-direction: row; 52 | flex-wrap: wrap; 53 | padding: var(--spacing); 54 | align-items: center; 55 | background: transparent; 56 | gap: var(--spacing); 57 | z-index: 4; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /fire/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fire", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.1", 7 | "cors": "^2.8.5", 8 | "csstype": "^3.0.7", 9 | "react": "^17.0.2", 10 | "react-beautiful-dnd": "^13.1.0", 11 | "react-dom": "^17.0.2", 12 | "react-google-login": "^5.2.2", 13 | "react-router-dom": "^5.2.0", 14 | "twgl.js": "^4.19.1", 15 | "web-vitals": "^0.2.4" 16 | }, 17 | "scripts": { 18 | "start": "set PORT=3000 && react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | }, 41 | "devDependencies": { 42 | "@testing-library/jest-dom": "^5.11.10", 43 | "@testing-library/react": "^11.2.3", 44 | "@testing-library/user-event": "^12.8.3", 45 | "@types/dom-mediacapture-record": "^1.0.7", 46 | "@types/react-beautiful-dnd": "^13.0.0", 47 | "@types/react-dom": "^17.0.3", 48 | "@types/react-router-dom": "^5.1.7", 49 | "react-scripts": "4.0.1", 50 | "typescript": "^4.2.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /doc/sprint4/PB.md: -------------------------------------------------------------------------------- 1 | | User Story | Points | Priority | 2 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | 3 | | As a teacher, I want to see my projects so that I can edit them. | 4 | Medium | 4 | | As a teacher, I want to create projects so that I can work on a new project. | 2 | Medium | 5 | | As a teacher, I want to delete projects so that I can remove projects that I don't want. | 2 | Medium | 6 | | As a teacher, I want to edit projects so that I can manipulate my previous work. | 4 | Medium | 7 | | As a teacher, I want to login so that I can save my projects to the cloud. | 8 | Low | 8 | | As a teacher, I want to upload projects to the cloud so that I can easily transfer my projects to a different device. | 4 | Low | 9 | | As a teacher, I want to download projects from the cloud so that I can easily transfer my projects and work on them. | 4 | Low | 10 | -------------------------------------------------------------------------------- /fire/src/components/mediaPool/mediaPool.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: mediaPool; 3 | background: var(--media-pool-background-color); 4 | padding: var(--spacing); 5 | display: flex; 6 | flex-direction: column; 7 | gap: var(--spacing); 8 | overflow-y: hidden; 9 | z-index: 5; 10 | } 11 | 12 | .mediaList { 13 | flex: 1; 14 | display: flex; 15 | flex-direction: column; 16 | flex-wrap: nowrap; 17 | overflow-y: auto; 18 | } 19 | 20 | .hbox { 21 | display: flex; 22 | flex-direction: row; 23 | justify-content: space-between; 24 | } 25 | 26 | .addFiles { 27 | width: 50px; 28 | height: 50px; 29 | } 30 | 31 | .card { 32 | background-color: var(--media-pool-card-background-color); 33 | border-radius: var(--border-radius); 34 | padding: var(--spacing); 35 | display: flex; 36 | align-items: center; 37 | justify-content: space-between; 38 | gap: var(--spacing); 39 | margin-bottom: 20px; 40 | } 41 | 42 | .img { 43 | width: 100px; 44 | } 45 | 46 | .cardCaption { 47 | color: var(--font-color); 48 | text-overflow: ellipsis; 49 | word-break: break-word; 50 | font-size: 12px; 51 | max-width: calc(100% - 100px - var(--spacing)); 52 | } 53 | 54 | .title { 55 | color: var(--font-color); 56 | font-weight: 400; 57 | font-size: 26px; 58 | line-height: 50px; 59 | } 60 | 61 | .button { 62 | align-self: flex-end; 63 | padding: 0px; 64 | margin: 0px -10px -10px 0px; 65 | height: auto; 66 | width: auto; 67 | } 68 | 69 | .button:hover { 70 | background: transparent; 71 | } 72 | 73 | .button span { 74 | font-size: 16px; 75 | color: #3a3a3a; 76 | } 77 | 78 | .button:hover span { 79 | font-size: 16px; 80 | color: #686868; 81 | } 82 | .loader { 83 | color:var(--font-color); 84 | font-size: large; 85 | } 86 | -------------------------------------------------------------------------------- /doc/sprint0/summary.md: -------------------------------------------------------------------------------- 1 | The main motive of our project is to introduce the internet world to an efficient video editing application. We want to engineer a web based video editor that allows users to manipulate videos from their computer, export the final video and save the project files to a cloud account.

2 | Key User Case 1 - David: A middle school student who likes to play games and upload his game play on Youtube. He’s not a professional video editor. He wants to become the popular dude in his school through the means of his videos. He plays fortnite daily and uploads his highlights to youtube. After coming back from school he plays fortnite for around 5 hours and then does any homework from school. 3 |

4 | Key User Case 2 - Jennifer: An elementary school math teacher who makes short lecture videos about simple math concepts and uploads her videos as a substitute for lectures for her students. She isn’t someone who has a high tech aptitude and is looking for an easy to use product to edit her lecture videos. 5 |

6 | Key Scenario 1: David is in search of an easy to use video editor to edit his most recent fortnite gameplay. He wants to crop the video part where he didn’t play as good as he wanted. He also wants to speed up some boring part of his gameplay like navigating through the terrain. 7 |

8 | Key Scenario 2: Jennifer wants a simple interface. Jennifer wants to add some short animations in her lectures to explain some tricky concepts easily. She records her video in one go and separates the video into short topics. She wants to segment from the long video and make short videos that she can download on her computer and upload later. 9 |

10 | Key Principles: Ease of use and cross platform-We are designing this project to have a simple user interface that works on most devices and operating systems, making it to be very accesible and easy to use for an user. -------------------------------------------------------------------------------- /fire/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 21 | 22 | 31 | Fire | Online Video Editor 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /doc/sprint4/sprint4.md: -------------------------------------------------------------------------------- 1 | # Meeting Goal 2 | 3 | Decide the user stories to be included in the fourth sprint, break user stories into sub-tasks, allocate tasks to every group member. 4 | 5 | # Sprint Goal 6 | 7 | User should be able to create, edit, delete, and store projects to the cloud. 8 | 9 | # Spikes 10 | 11 | None so far 12 | 13 | # Team Capacity 14 | 15 | 36 16 | 17 | # Sprint Backlog 18 | 19 | | User Story | Sub Tasks | Story Points | 20 | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | 21 | | As a teacher, I want to see my projects so that I can edit them. | - Creating UI for the Login and Project Management
- Fix UI of project management based on updated code| 4 | 22 | | As a teacher, I want to create projects so that I can work on a new project. | - Create endpoint for inserting project data to the database | 2 | 23 | | As a teacher, I want to delete projects so that I can remove projects that I don't want. | - Delete project | 2 | 24 | | As a teacher, I want to edit projects so that I can manipulate my previous work. | - Create endpoint for editing a project | 4 | 25 | | As a teacher, I want to login so that I can save my projects to the cloud. | - OAuth Login
- Display saved projects | 8 | 26 | | As a teacher, I want to upload projects to the cloud so that I can easily transfer my projects to a different device. | - Database Setup
- Create PUT endpoint | 8 | 27 | | As a teacher, I want to download projects from the cloud so that I can easily transfer my projects and work on them. | - Load project when selected | 4 | 28 | 29 | | Bugs | Points | 30 | | --- | --- | 31 | | Segment splitting supports keyframes | 4 | 32 | 33 | 34 | # Participants 35 | 36 | - Rohan Poojary (poojaryr) 37 | - Julian de Rushe (derushej) 38 | - Mohammad Tahvili (tahvilim) 39 | - Alees Ahmad Goraya (gorayaal) 40 | - Tomasz Cieslak (cieslak4) 41 | - Jagdev Singh Jhajj (jhajjjag) 42 | - Bhavik Kothari (kothar33) 43 | -------------------------------------------------------------------------------- /fire/src/routes/login.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import styles from "./login.module.css"; 3 | import GoogleLogin from 'react-google-login'; 4 | import axios from "axios"; 5 | 6 | export default function Login(props: 7 | { 8 | projectUser: string; 9 | setProjectUser: (user: string) => void; 10 | }) { 11 | 12 | 13 | const unsuccessfulLogin = (response: any) => { 14 | console.log("Login process terminated"); 15 | } 16 | 17 | const handleLogin = (response: any) => { 18 | sessionStorage.setItem("token", response.tokenId); 19 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 20 | instance.get("/getEmail", { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }).then((res) => { 21 | props.setProjectUser(res.data.email); 22 | }); 23 | } 24 | 25 | return ( 26 |
27 |
28 |
29 |
30 | 31 | {/* */} 34 | ( 37 | 40 | )} 41 | buttonText="Login" 42 | onSuccess={handleLogin} 43 | onFailure={unsuccessfulLogin} 44 | cookiePolicy={'single_host_origin'} 45 | /> 46 |
47 |
48 |
); 49 | } 50 | -------------------------------------------------------------------------------- /presentation.md: -------------------------------------------------------------------------------- 1 | # Final Project Presentation Marking Rubric 2 | Team Name: The Nerds 3 | 4 | ## Part I - Live Presentation - 30 marks 5 | ### a. Product Demo [10 marks]: 6 | Product introduction: 0-1 marks [1=Product introduced, 0=No introduction] 7 | 8 | Your mark: 1 9 | 10 | Features Demo: up to 3 marks for each major feature presented 11 | - Feature 1 presentation: 0-3 marks [3=Excellent, 2=Adequate, 1=Inadequate, 0=No feature] 12 | 13 | Your mark: 3 14 | - Feature 2 presentation: 0-3 marks [3=Excellent, 2=Adequate, 1=Inadequate, 0=No feature] 15 | 16 | Your mark: 3 17 | - Feature 3 presentation: 0-3 marks [3=Excellent, 2=Adequate, 1=Inadequate, 0=No feature] 18 | 19 | Your mark: 3 20 | 21 | ### b. Process [10 marks]: 22 | Team Introduction: 0-4 marks 23 | - [4=Presentation looks and sounds professional, all members are present] 24 | - [Each missing person, minus 1 mark] 25 | - [Note due to this year's special situation: you may or may not show your person in video. However voice participation is a mandatory minimum] 26 | 27 | Your mark:4 28 | 29 | Team communication, processes, standards, and techniques: 0-6 marks 30 | - [Each element 2 marks if presented professionally, 1 mark if presented adequately, 0 otherwise] 31 | 32 | Your mark: 6 33 | 34 | ### c. Software Architecture [10 marks]: 35 | Software Architecture 36 | - 0-4 marks 37 | - [4=Architecture presented clearly, illustrated with sufficient diagrams, components and responsibilities shown clearly] 38 | - [-1 mark for each missing component] 39 | 40 | Your mark: 4 41 | 42 | Technologies used and technical challenges 43 | - 0-6 marks 44 | - [6=Technologies presented clearly, illustrated with sufficient diagrams/links/examples, technical challenges indicated clearly 45 | - Also a clear description what the team did to overcome those challenges] 46 | - [-2 marks for each missing component, -1 mark is component not presented clearly] 47 | 48 | Your mark: 6 49 | ## Part II - Presentation (5 marks) 50 | Participation [5 marks] 51 | 52 | - Team asked at least two meaningful questions to other teams 53 | - Q1 (2 marks) 54 | - Q2 (2 marks) 55 | - Questions were asked using a professional tone and body language (1 mark) 56 | 57 | Your mark: 5 58 | 59 | Total mark: 35/35 60 | -------------------------------------------------------------------------------- /fire/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { KeyFrame, Segment } from "../model/types"; 2 | 3 | interface Property { 4 | start: number; 5 | startTime: number; 6 | end: number; 7 | endTime: number; 8 | } 9 | 10 | export const lerp = (start: number, end: number, t: number) => { 11 | return (end - start) * t + start; 12 | } 13 | 14 | export const inverseLerp = (value: number, start: number, end: number) => { 15 | if (end === start) return 1; 16 | return Math.min(Math.max((value - start) / (end - start), 0), 1); 17 | } 18 | 19 | export const calculateProperties = (segment: Segment | null, timestamp: number): KeyFrame => { 20 | timestamp -= segment ? segment.start : 0; 21 | 22 | const PROPERTY_NAMES = ['x', 'y', 'scaleX', 'scaleY', 'trimLeft', 'trimRight', 'trimBottom', 'trimTop']; 23 | let properties: Property[] = []; 24 | for (const property of PROPERTY_NAMES) { 25 | properties.push( 26 | { 27 | // @ts-ignore 28 | start: segment ? segment.keyframes[0][property] ?? 0 : 0, 29 | startTime: 0, 30 | //@ts-ignore 31 | end: segment ? segment.keyframes[0][property] ?? 0 : 0, 32 | endTime: 0 33 | } 34 | ) 35 | } 36 | 37 | if (segment) { 38 | for (let i = 0; i < segment.keyframes.length; i++) { 39 | const frame = segment.keyframes[i]; 40 | 41 | for (let j = 0; j < PROPERTY_NAMES.length; j++) { 42 | //@ts-ignore 43 | if (frame[PROPERTY_NAMES[j]] !== undefined) { 44 | properties[j].start = properties[j].end; 45 | properties[j].startTime = properties[j].endTime; 46 | //@ts-ignore 47 | properties[j].end = frame[PROPERTY_NAMES[j]]; 48 | properties[j].endTime = frame.start; 49 | } 50 | 51 | } 52 | if (frame.start > timestamp) break; 53 | } 54 | } 55 | 56 | let output = { start: 0 } 57 | for (let i = 0; i < PROPERTY_NAMES.length; i++) { 58 | //@ts-ignore 59 | output[PROPERTY_NAMES[i]] = lerp(properties[i].start, properties[i].end, inverseLerp(timestamp, properties[i].startTime, 60 | properties[i].endTime)); 61 | } 62 | 63 | return output; 64 | } -------------------------------------------------------------------------------- /doc/sprint2/sprint2.md: -------------------------------------------------------------------------------- 1 | # Meeting Goal 2 | 3 | Decide the user stories to be included in the second sprint, break user stories into sub-tasks, allocate tasks to every group member. 4 | 5 | # Sprint Goal 6 | 7 | Adding basic video editing capabilities to the video editor. The user should be able to select, move, delete, trim, scale and position their video segments. 8 | 9 | # Spikes 10 | 11 | None so far 12 | 13 | # Team Capacity 14 | 15 | 68 16 | 17 | # Sprint Backlog 18 | 19 | | User Story | Sub Tasks | Story Points | 20 | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | 21 | | As a secondary school student, I want to see a timeline so that I can see when a video clip starts and ends. | - Timeline UI
- Drag and Drop
- Displaying video segments in tracks | 8 | 22 | | As a secondary school student, I want to split videos in the timeline so that I cut the video into 2 different clips. | - Create segment split
-Hook the split button to allow for segment splitting | 8 | 23 | | As a secondary school student, I want to delete videos in the timeline so that I can get rid of the clips I don't want. | - Hook delete button to allow deleting clips | 8 | 24 | | As a secondary school student, I want to move videos in the timeline so that I can change its postition. | - Add Move Functionality | 8 | 25 | | As a secondary school student, I want to trim videos in the timeline so that I can change the length of a clip. | - Allow users to trim videos | 8 | 26 | | As a secondary school student, I want to scale videos so that I can resize the video. | - Zoom Functionality | 4 | 27 | | As a secondary school student, I want to position videos so that I can overlap videos. | - Create the buttons for positioning the video
- Add action to the position buttons| 4 | 28 | 29 | | Bugs | Points | 30 | | --- | --- | 31 | | Fix model code with React Hooks| 8 | 32 | | Update timeline to support multiple tracks| 8 | 33 | | Fix playback pause bug| 4 | 34 | 35 | 36 | # Participants 37 | 38 | - Rohan Poojary (poojaryr) 39 | - Julian de Rushe (derushej) 40 | - Mohammad Tahvili (tahvilim) 41 | - Alees Ahmad Goraya (gorayaal) 42 | - Tomasz Cieslak (cieslak4) 43 | - Jagdev Singh Jhajj (jhajjjag) 44 | - Bhavik Kothari (kothar33) 45 | -------------------------------------------------------------------------------- /doc/sprint3/sprint3.md: -------------------------------------------------------------------------------- 1 | # Meeting Goal 2 | 3 | Decide the user stories to be included in the third sprint, break user stories into sub-tasks, allocate tasks to every group member. 4 | 5 | # Sprint Goal 6 | 7 | Users should be able to overlay multiple videos, crop them, add, delete, update keyframes, and download their edited videos. 8 | 9 | # Spikes 10 | 11 | None so far 12 | 13 | # Team Capacity 14 | 15 | 51 16 | 17 | # Sprint Backlog 18 | 19 | | User Story | Sub Tasks | Story Points | 20 | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | 21 | | As a secondary school student, I want to crop videos so that I can remove unwanted parts from video. | - Create the ability to crop segments
- Hook segment cropping with the canvas video preview | 4 | 22 | | As a secondary school student, I want to add keyframes to the timeline so that I can create transitions and animations. | - Add Keyframe bar UI to Timeline
- Add Keyframe UI to Properties Component | 8 | 23 | | As a secondary school student, I want keyframes to change video properties when the video is played so that transitions and animations are played. | - Keyframe Modification
- Keyframe Playback Interpolation | 8 | 24 | | As a secondary school student, I want to render my project to a video so that I can share my edited videos. | - Render Video | 4 | 25 | | As a secondary school student, I want to see an estimate of how long rendering will take so that I know how much time I have to wait. | - UI component for rendering bar
- Display Rendering Percentage | 4 | 26 | | As a secondary school student, I want to save rendered videos so that I can view them later. | - Make Export page UI
- Save the rendered videos locally | 4 | 27 | | As a secondary school student, I want to use multiple tracks so that I can overlay videos. | - Fix Render so that full video is rendered
- Drag and drop UI updates
- Timeline CSS updates
- Add multitrack support to move | 16 | 28 | 29 | | Bugs | Points | 30 | | --- | --- | 31 | | Clean UI elements and fix CSS for better user experience and better responsiveness | 4 | 32 | | Drag and drop file(s) in the Media Pool | 2 | 33 | 34 | 35 | # Participants 36 | 37 | - Rohan Poojary (poojaryr) 38 | - Julian de Rushe (derushej) 39 | - Mohammad Tahvili (tahvilim) 40 | - Alees Ahmad Goraya (gorayaal) 41 | - Tomasz Cieslak (cieslak4) 42 | - Jagdev Singh Jhajj (jhajjjag) 43 | - Bhavik Kothari (kothar33) 44 | -------------------------------------------------------------------------------- /fire/src/components/timeline/timeline.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: timeline; 3 | background: var(--timeline-background-color); 4 | padding: 0px 0px 0px 0px; 5 | overflow: auto; 6 | } 7 | 8 | .tracks { 9 | position: relative; 10 | min-height: 100%; 11 | } 12 | 13 | .timebar { 14 | width: 100%; 15 | display: flex; 16 | } 17 | 18 | .time { 19 | color: var(--font-color); 20 | font-size: 14px; 21 | -moz-user-select: none; 22 | -khtml-user-select: none; 23 | -webkit-user-select: none; 24 | -ms-user-select: none; 25 | user-select: none; 26 | cursor: pointer; 27 | } 28 | 29 | .indicator { 30 | background: #ff0000; 31 | width: 1px; 32 | height: 100%; 33 | margin: auto; 34 | } 35 | 36 | .pointer { 37 | position: absolute; 38 | z-index: 4; 39 | background: red; 40 | left: 0; 41 | top: 0; 42 | bottom: 0; 43 | } 44 | 45 | .track { 46 | height: 80px; 47 | display: flex; 48 | flex-direction: row; 49 | justify-content: flex-start; 50 | border-bottom: 1px dashed #383838; 51 | align-items: center; 52 | background: var(--timeline-background-color); 53 | } 54 | 55 | .card { 56 | border-radius: 2px; 57 | transition: box-shadow 0.25s, border-color 0.25s; 58 | } 59 | 60 | .fullCard { 61 | display: inline-block; 62 | width: auto; 63 | height: auto; 64 | } 65 | 66 | .keyframeCard { 67 | width: 100%; 68 | background: #202020; 69 | border-radius: 2px; 70 | height: 20px; 71 | } 72 | 73 | .keyframeBtn, 74 | .keyframeBtn:hover { 75 | width: 6px; 76 | height: 6px; 77 | background: #fff; 78 | border-radius: 0px; 79 | margin-left: -2.55px; 80 | margin-top: 7px; 81 | padding: 0px; 82 | transition: 0s; 83 | position: absolute; 84 | } 85 | 86 | .ruler { 87 | height: 40px; 88 | position: sticky; 89 | top: 0; 90 | z-index: 1; 91 | align-items: baseline; 92 | } 93 | 94 | .s10 { 95 | text-align: center; 96 | position: relative; 97 | padding-top: 10px; 98 | } 99 | 100 | .sec { 101 | border-right: 1px solid #383838; 102 | top: 0; 103 | height: 6px; 104 | position: absolute; 105 | } 106 | 107 | .sec:nth-of-type(1) { 108 | left: 20%; 109 | } 110 | 111 | .sec:nth-of-type(2) { 112 | left: 40%; 113 | } 114 | 115 | .sec:nth-of-type(3) { 116 | left: 60%; 117 | } 118 | 119 | .sec:nth-of-type(4) { 120 | left: 80%; 121 | } 122 | 123 | .sec:nth-of-type(5) { 124 | height: 14px; 125 | right: 0px; 126 | } 127 | 128 | .rulerCon { 129 | display: table; 130 | table-layout: fixed; 131 | width: 100%; 132 | } 133 | 134 | .move { 135 | cursor: move !important; 136 | } 137 | -------------------------------------------------------------------------------- /doc/sprint3/PB.md: -------------------------------------------------------------------------------- 1 | | User Story | Points | Priority | 2 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | 3 | | As a secondary school student, I want to crop videos so that I can remove unwanted parts from video. | 4 | Medium | 4 | | As a secondary school student, I want to add keyframes to the timeline so that I can create transitions and animations. | 8 | Medium | 5 | | As a secondary school student, I want keyframes to change video properties when the video is played so that transitions and animations are played. | 8 | Medium | 6 | | As a secondary school student, I want to render my project to a video so that I can share my edited videos. | 4 | Medium | 7 | | As a secondary school student, I want to see an estimate of how long rendering will take so that I know how much time I have to wait. | 4 | Medium | 8 | | As a secondary school student, I want to save rendered videos so that I can view them later. | 4 | Medium | 9 | | As a secondary school student, I want to use multiple tracks so that I can overlay videos. 10 | | 16 | Medium | 11 | | As a teacher, I want to see my projects so that I can edit them. | 4 | Medium | 12 | | As a teacher, I want to create projects so that I can work on a new project. | 2 | Medium | 13 | | As a teacher, I want to delete projects so that I can remove projects that I don't want. | 2 | Medium | 14 | | As a teacher, I want to edit projects so that I can manipulate my previous work. | 4 | Medium | 15 | | As a teacher, I want to login so that I can save my projects to the cloud. | 8 | Medium | 16 | | As a teacher, I want to upload projects to the cloud so that I can easily transfer my projects to a different device. | 4 | Low | 17 | | As a teacher, I want to download projects from the cloud so that I can easily transfer my projects and work on them. | 4 | Low | 18 | -------------------------------------------------------------------------------- /fire/src/model/projectManager.tsx: -------------------------------------------------------------------------------- 1 | import MediaManager from "./mediaManager"; 2 | import { useEffect, useState } from "react"; 3 | import { Project } from "./types"; 4 | import axios from "axios"; 5 | 6 | export default function ProjectManager() { 7 | const [projects, setProjects] = useState([]); 8 | const [currentProject, setCurrentProject] = useState(0); 9 | const [projectUser, setProjectUser] = useState(""); 10 | const [projectId, setProjectId] = useState(""); 11 | const [projectName, setProjectName] = useState(""); 12 | const [projectWidth, setProjectWidth] = useState(1920); 13 | const [projectHeight, setProjectHeight] = useState(1080); 14 | const [projectFramerate, setProjectFramerate] = useState(30); 15 | const [projectDuration, setProjectDuration] = useState(0); 16 | 17 | useEffect(() => { 18 | if (currentProject >= projects.length) return; 19 | setProjectId(projects[currentProject]._id); 20 | setProjectName(projects[currentProject].name); 21 | setProjectWidth(projects[currentProject].width); 22 | setProjectHeight(projects[currentProject].height); 23 | setProjectFramerate(projects[currentProject].framerate); 24 | setProjectDuration(projects[currentProject].duration); 25 | }, [currentProject, projects]); 26 | 27 | useEffect(() => { 28 | if (sessionStorage.getItem("token") != null) { 29 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 30 | instance.get("/getEmail", { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }).then((res) => { 31 | setProjectUser(res.data.email); 32 | }); 33 | } 34 | }, []); 35 | 36 | useEffect(() => { 37 | if (projectUser === "") return; 38 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 39 | instance.get("/getProjects", { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }).then((res) => { 40 | setProjects(res.data); 41 | }); 42 | }, [projectUser]); 43 | 44 | return ; 62 | } 63 | -------------------------------------------------------------------------------- /fire/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | margin: 0; 8 | font-family: "Helvetica Neue", -apple-system, BlinkMacSystemFont, "Segoe UI", 9 | "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", 10 | sans-serif; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | background: var(--background-color); 14 | --spacing: 15px; 15 | --border-radius: 5px; 16 | --timeline-background-color: #0a0a0a; 17 | --media-pool-background-color: #161618; 18 | --media-pool-card-background-color: #0a0a0a; 19 | --media-player-background-color: #111111; 20 | --font-color: #838383; 21 | --background-color: #101011; 22 | --icon-background-color: #0a0a0a; 23 | --icon-color: #838383; 24 | --icon-background-hover-color: #030303; 25 | --scrollbar-color: #838383; 26 | --highlight-color: #fff; 27 | --black-color: #000; 28 | overflow-x: hidden; 29 | user-select: none; 30 | } 31 | 32 | button { 33 | height: 50px; 34 | width: 50px; 35 | background: var(--icon-background-color); 36 | border-radius: 100%; 37 | padding: 5px; 38 | border: none; 39 | cursor: pointer; 40 | transition: 0.25s; 41 | } 42 | 43 | button:hover { 44 | background: var(--icon-background-hover-color); 45 | } 46 | 47 | button:focus { 48 | box-shadow: 0px 0px 5px 2px #cacaca; 49 | border-radius: 100px; 50 | outline: none; 51 | } 52 | 53 | ::-webkit-scrollbar-corner { 54 | background: var(--timeline-background-color); 55 | } 56 | 57 | ::-webkit-scrollbar { 58 | width: 30px; 59 | height: 30px; 60 | } 61 | 62 | ::-webkit-scrollbar-track { 63 | background: transparent; 64 | border-radius: var(--border-radius); 65 | } 66 | 67 | ::-webkit-scrollbar-thumb { 68 | box-shadow: inset 0 0 15px 15px var(--scrollbar-color); 69 | border: solid 10px transparent; 70 | border-radius: 100px; 71 | } 72 | 73 | .material-icons.md-18 { 74 | font-size: 18px; 75 | } 76 | .material-icons.md-24 { 77 | font-size: 24px; 78 | } 79 | .material-icons.md-36 { 80 | font-size: 36px; 81 | } 82 | .material-icons.md-48 { 83 | font-size: 48px; 84 | } 85 | 86 | .material-icons { 87 | color: var(--font-color); 88 | } 89 | 90 | .draggedOn { 91 | outline: 2px dashed #313131; 92 | outline-offset: -10px; 93 | } 94 | 95 | @media (max-width: 1100px) { 96 | button { 97 | height: 25px; 98 | width: 25px; 99 | background: var(--icon-background-color); 100 | border-radius: 100%; 101 | padding: 5px; 102 | border: none; 103 | cursor: pointer; 104 | } 105 | button .material-icons { 106 | font-size: 14px; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /doc/sprint1/sprint1.md: -------------------------------------------------------------------------------- 1 | # Meeting Goal 2 | 3 | Decide the user stories to be included in the first sprint, allocate tasks to every group member. 4 | 5 | # Sprint Goal 6 | 7 | Build the UI of the video editor and allow users to upload, manage and preview videos using the rendering pipeline. 8 | 9 | # Spikes 10 | 11 | None so far 12 | 13 | # Team Capacity 14 | 15 | 41 16 | 17 | # Sprint Backlog 18 | 19 | | User Story | Sub Tasks | Story Points | 20 | | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------ | 21 | | As a teacher, I want to select videos from my computer on the website so they can be edited. | - Create a media pool component to display uploaded videos.
- Select file from the local system to the app. | 8 | 22 | | As a teacher, I want to see a live preview of a video I select from the media pool so that I can easily identify the video. | - Delete uploaded video from media pool re-arrange uploaded videos.
- Preview selected video from the media pool | 8 | 23 | | As a teacher, I want to see a live preview of the rendered project so that I can visualize the adjustments that have been made. | - Create webGL boilerplate. | 8 | 24 | | As a teacher, I want to see a loading indicator for videos that are loading so that I know the editor is working on it. | - Load video data from filehandle.
- Create Loading Indicator UI. | 8 | 25 | | As a teacher, I want to see playback controls for videos so that I can easily view the video. | - Create a trackbar component.
- Create button controls. | 8 | 26 | | As a teacher, I want to see an About page so that I see contact information and version number. | - Create about route. | 1 | 27 | 28 | # Participants 29 | 30 | - Rohan Poojary (poojaryr) 31 | - Julian de Rushe (derushej) 32 | - Mohammad Tahvili (tahvilim) 33 | - Alees Ahmad Goraya (gorayaal) 34 | - Tomasz Cieslak (cieslak4) 35 | - Jagdev Singh Jhajj (jhajjjag) 36 | - Bhavik Kothari (kothar33) 37 | -------------------------------------------------------------------------------- /fire/src/routes/exportPage.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import styles from "./exportPage.module.css"; 3 | import { useState } from "react"; 4 | 5 | declare global { 6 | interface HTMLCanvasElement { 7 | captureStream(frameRate?: number): MediaStream; 8 | } 9 | } 10 | 11 | export default function ExportPage(props: any) { 12 | const [load, setLoad] = useState(0); 13 | 14 | function AddProject() { 15 | if (props.trackList.length > 1) { 16 | var segment; 17 | for (var i = 0; i < props.trackList.length; i++) { 18 | if (props.trackList[i].length > 0) { 19 | segment = props.trackList[i][0]; 20 | } 21 | } 22 | 23 | setLoad(0); 24 | if (props.isRecordingRef.current) 25 | setLoad(Math.round((100 * props.currentTime) / props.projectDuration)); 26 | 27 | return ( 28 |
29 |
    30 |
  • 31 |
    37 |

    Untitled

    38 |
    39 | 40 | 45 | 46 |
    47 |
  • 48 |
49 |
50 | ); 51 | } 52 | return
No project to export
; 53 | } 54 | 55 | return ( 56 |
57 |
58 |
59 | 60 | 61 | 62 |
63 |

Fire

64 |

Video Editor

65 |
66 |
67 | 68 | layers Current Projects 69 | 70 | 71 | save_alt Exported Files 72 | 73 |
74 | 79 |
80 | 81 |
82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /fire/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fire/src/components/elements/properties.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | grid-area: elements; 3 | display: flex; 4 | flex-direction: column; 5 | gap: 10px; 6 | margin-right: -350px; 7 | padding: 20px 0px; 8 | padding-bottom: 60px; 9 | min-width: 350px; 10 | height: 100%; 11 | overflow-y: auto; 12 | background: var(--media-pool-background-color); 13 | transition: margin-right 0.25s; 14 | border: none; 15 | } 16 | 17 | .slideIn { 18 | margin-right: 0vw; 19 | } 20 | 21 | .title { 22 | padding: 0px 80px 0px 15px; 23 | color: var(--font-color); 24 | font-weight: 400; 25 | font-size: 26px; 26 | line-height: 50px; 27 | } 28 | 29 | .keyframePrev span, 30 | .keyframeNext span { 31 | color: rgb(102, 102, 102); 32 | font-size: 18px; 33 | } 34 | .keyframeBtn span { 35 | color: rgb(102, 102, 102); 36 | font-size: 10px; 37 | margin-top: 0px; 38 | } 39 | .keyframePrev { 40 | width: 12px; 41 | height: 16px; 42 | background: transparent; 43 | border-radius: 0px; 44 | margin: 0px 6px; 45 | padding: 0px; 46 | text-align: center; 47 | float: right; 48 | margin-top: 2px; 49 | } 50 | .keyframeNext { 51 | width: 12px; 52 | height: 16px; 53 | background: transparent; 54 | border-radius: 0px; 55 | padding: 0px; 56 | text-align: center; 57 | float: right; 58 | margin-top: 2px; 59 | } 60 | .keyframeBtn { 61 | width: 10px; 62 | height: 16px; 63 | background: transparent; 64 | border-radius: 0px; 65 | padding: 0px; 66 | text-align: center; 67 | float: right; 68 | margin-top: 3.8px; 69 | } 70 | 71 | .keyframePrev:focus, 72 | .keyframeNext:focus, 73 | .keyframeBtn:focus { 74 | box-shadow: none; 75 | } 76 | .keyframePrev:hover, 77 | .keyframeNext:hover, 78 | .keyframeBtn:hover { 79 | background: transparent; 80 | } 81 | 82 | .tags { 83 | padding: 10px 80px 0px 15px; 84 | color: var(--font-color); 85 | font-size: 15px; 86 | border-top: 1px solid #3838385d; 87 | margin: 0px; 88 | } 89 | 90 | .effectBox { 91 | padding: 10px 80px 0px 15px; 92 | border-top: 1px solid #3838382d; 93 | display: grid; 94 | grid-template-columns: 1fr 1fr; 95 | margin-left: 15px; 96 | } 97 | .effectBox:last-of-type { 98 | border-bottom: 1px solid #3838382d; 99 | padding-bottom: 10px; 100 | margin-bottom: 20px; 101 | } 102 | 103 | .effectBox input { 104 | background: transparent; 105 | border: 0px; 106 | color: var(--highlight-color); 107 | border-bottom: 1px dashed #3838382d; 108 | padding: 0px 5px; 109 | text-align: center; 110 | } 111 | 112 | .effectBox input[type="number"]::-webkit-inner-spin-button, 113 | .effectBox input[type="number"]::-webkit-outer-spin-button { 114 | -webkit-appearance: none; 115 | } 116 | .inputBtn { 117 | background: transparent; 118 | padding: 2px 5px 17px 5px; 119 | color: var(--font-color); 120 | height: 15px; 121 | width: 18px; 122 | } 123 | 124 | .tag { 125 | font-size: 12px; 126 | color: var(--font-color); 127 | } 128 | 129 | .tags:nth-of-type(1) { 130 | border-top: 0px; 131 | } 132 | 133 | .inputTag { 134 | width: 45px; 135 | } 136 | 137 | @media (max-width: 1100px) { 138 | .container { 139 | margin-right: 0vw; 140 | display: none; 141 | } 142 | 143 | .slideIn { 144 | display: flex; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /fire/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /fire/src/components/controls/controls.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent } from "react"; 2 | import styles from "./controls.module.css"; 3 | 4 | export default function Controls( 5 | { 6 | playVideo, 7 | pauseVideo, 8 | isPlaying, 9 | currentTime, 10 | projectDuration, 11 | setCurrentTime, 12 | splitVideo, 13 | deleteSelectedSegment, 14 | setScaleFactor, 15 | scaleFactor 16 | }: 17 | { 18 | playVideo: any, 19 | pauseVideo: any, 20 | isPlaying: boolean, 21 | currentTime: number, 22 | projectDuration: number, 23 | splitVideo: any; 24 | setCurrentTime: (timestamp: number) => void, 25 | deleteSelectedSegment: any 26 | setScaleFactor: (scale: number) => void, 27 | scaleFactor: number 28 | } 29 | ) { 30 | const togglePlaying = () => { 31 | if (isPlaying) { 32 | pauseVideo(); 33 | } else { 34 | playVideo(); 35 | } 36 | }; 37 | 38 | const increaseScale = () => { 39 | setScaleFactor(Math.min(1, scaleFactor * 1.2)) 40 | } 41 | 42 | const decreaseScale = () => { 43 | setScaleFactor(Math.max(0.0001, scaleFactor * 0.8)) 44 | } 45 | 46 | const onSeek = (event: ChangeEvent) => { 47 | setCurrentTime(+event.target.value * projectDuration); 48 | } 49 | 50 | const createSplit = () => { 51 | splitVideo(currentTime); 52 | }; 53 | 54 | return ( 55 |
56 | 59 | 64 | 67 | 76 | 79 | 82 | 85 | 88 | 91 | 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /fire/src/routes/exportPage.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | color: var(--font-color); 3 | background-color: var(--timeline-background-color); 4 | overflow:auto; 5 | height:100vh; 6 | width:100vw; 7 | } 8 | 9 | .projectBox { 10 | background-position: center center; 11 | background-repeat: no-repeat; 12 | background-size: 100%; 13 | height:250px; 14 | border-radius:5px; 15 | margin:15px; 16 | float:left; 17 | display:flex; 18 | align-items: flex-end; 19 | cursor: pointer; 20 | } 21 | 22 | .projectBox h2{ 23 | font-weight: normal; 24 | color: rgb(224, 224, 224); 25 | padding:15px; 26 | font-size:18px; 27 | width:100%; 28 | background:linear-gradient(0deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%); 29 | } 30 | 31 | .sidebar { 32 | background: var(--media-pool-background-color); 33 | width:calc(100% - 60px); 34 | padding:30px; 35 | } 36 | 37 | .new, .downlaod{position: fixed;bottom:30px;right:30px;z-index:99;background:var(--media-pool-background-color);padding:11px 46px 46px 11px;} 38 | .downlaod{padding:12px 45px 45px 12px;} 39 | .new span{font-size:36px;} 40 | .downlaod span{font-size:32px;} 41 | 42 | .button{ 43 | background: var(--media-pool-background-color); 44 | border-radius:25px; 45 | padding:25px 10px; 46 | color: var(--font-color); 47 | display:flex; 48 | width:100%; 49 | align-items: center; 50 | } 51 | 52 | ::-webkit-scrollbar { 53 | width: 10px; 54 | height: 10px; 55 | } 56 | 57 | .main {display: block; width:calc(100% - 100px);padding:50px;min-height:calc(100vh - 100px)} 58 | 59 | .vbar{display:flex;align-items: center;width:100%;margin-bottom:40px;} 60 | .logo{width:65px;padding-top:10px;} 61 | .vbar h1{font-weight: normal;font-size:26px;} 62 | 63 | .btn, .active {color: var(--font-color);display:flex;align-items: center;text-decoration: none;padding:15px 20px;background:rgba(0, 0, 0, 0.151);border-radius:100px;margin:15px 0px;} 64 | .btn span, .active span{margin-right:10px;} 65 | .active{background:rgba(68, 68, 68, 0.151);} 66 | 67 | .mediaList { 68 | flex: 1; 69 | display: flex; 70 | flex-direction: column; 71 | flex-wrap: nowrap; 72 | width: 100%; 73 | height: auto; 74 | } 75 | .card { 76 | background-color: var(--media-pool-card-background-color); 77 | border-radius: var(--border-radius); 78 | display: flex; 79 | align-items: center; 80 | justify-content: space-between; 81 | list-style: none; 82 | padding:0px; 83 | gap: var(--spacing); 84 | margin-bottom: 20px; 85 | } 86 | .card li{width:100%;} 87 | .imageProperties { 88 | float: left; 89 | max-width: 300px; 90 | width:100%; 91 | border-radius:5px; 92 | overflow:hidden; 93 | height: 150px; 94 | background-size: cover; 95 | background-position: center center; 96 | position: relative; 97 | margin-right: 30px; 98 | } 99 | 100 | .process {display:grid; 101 | grid-template-columns: 2fr 7fr 1fr;position:relative; top:90px;align-items: center;gap: 20px;} 102 | 103 | .progressBar { 104 | width:100%; 105 | } 106 | 107 | ::-webkit-progress-bar{background-color:rgb(255, 80, 11);} 108 | 109 | h3{font-weight: normal; position: relative;top:60px;} 110 | 111 | .label, .render { 112 | color: var(--font-color); 113 | } 114 | 115 | @media (min-width: 1100px) { 116 | .sidebar { 117 | height: calc(100vh - 60px); 118 | } 119 | .projectBox{ 120 | width:calc((100% / 3) - 30px); 121 | } 122 | .container { 123 | display:grid; 124 | grid-template-columns: 1fr 4fr; 125 | } 126 | .login{ 127 | display:flex; 128 | align-items: center; 129 | } 130 | } -------------------------------------------------------------------------------- /fire/src/routes/projects.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | color: var(--font-color); 3 | background-color: var(--timeline-background-color); 4 | width:100vw; 5 | overflow:auto; 6 | height:100vh; 7 | } 8 | 9 | .projectBox { 10 | background-color: var(--media-pool-background-color); 11 | background-position: center center; 12 | background-repeat: no-repeat; 13 | background-size: 100%; 14 | height:250px; 15 | width:100%; 16 | border-radius:5px; 17 | margin:15px 0px; 18 | float:left; 19 | display:flex; 20 | align-items: flex-end; 21 | cursor: pointer; 22 | overflow:hidden; 23 | } 24 | 25 | .boxShadow{ 26 | width:100%; 27 | background:linear-gradient(0deg, rgba(0,0,0,1) 0%, rgba(0,0,0,0) 100%); 28 | display:grid; 29 | grid-template-columns: 7fr 1fr 1fr; 30 | } 31 | 32 | .projectBox h2{ 33 | font-weight: normal; 34 | color: rgb(224, 224, 224); 35 | padding:15px; 36 | font-size:18px; 37 | } 38 | 39 | .projectBox button{background:transparent;padding:0px;margin:0px;width:auto;height:auto;} 40 | .projectBox span{font-size:20px;} 41 | 42 | .sidebar { 43 | background: var(--media-pool-background-color); 44 | width:calc(100% - 60px); 45 | padding:30px; 46 | } 47 | 48 | .popup {position: fixed; top:0px; right:0px; width:100vw;height:100vh;background:rgba(0, 0, 0, 0.308);z-index:999;display:flex;align-items: center;vertical-align: center;} 49 | .popup h2{font-weight: normal;} 50 | .popupClose {float:right;margin-top:-40px;margin-right:-10px;} 51 | .popup form{padding-top:20px;} 52 | .popup button{margin-bottom:-20px;} 53 | .popup form div{display:grid;grid-template-columns: 1fr 4fr;column-gap: 20px;margin-bottom:20px;align-items: center;} 54 | .popup form input{background:transparent;border:0px;border-bottom:1px solid #525252;padding:10px;color:#fff;} 55 | .popupContainer {width:100%;max-width: 500px; background:var(--media-pool-background-color);padding:30px;border-radius:5px;margin:auto;box-shadow:0px 0px 8px #000;} 56 | 57 | .new, .downlaod{position: fixed;bottom:30px;right:30px;z-index:99;background:var(--media-pool-background-color);padding:11px 46px 46px 11px;} 58 | .downlaod{padding:12px 45px 45px 12px;} 59 | .new span{font-size:36px;} 60 | .downlaod span{font-size:32px;} 61 | 62 | .button{ 63 | background: var(--media-pool-background-color); 64 | border-radius:25px; 65 | padding:25px 10px; 66 | color: var(--font-color); 67 | display:flex; 68 | width:100%; 69 | align-items: center; 70 | } 71 | 72 | ::-webkit-scrollbar { 73 | width: 10px; 74 | height: 10px; 75 | } 76 | 77 | .main {display: block; width:calc(100% - 100px);padding:50px;min-height:calc(100vh - 100px)} 78 | 79 | .vbar{display:flex;align-items: center;width:100%;margin-bottom:40px;} 80 | .logo{width:65px;padding-top:10px;} 81 | .vbar h1{font-weight: normal;font-size:26px;} 82 | 83 | .btn, .active {color: var(--font-color);display:flex;align-items: center;text-decoration: none;padding:15px 20px;background:rgba(0, 0, 0, 0.151);border-radius:100px;margin:15px 0px;} 84 | .btn span, .active span{margin-right:10px;} 85 | .active{background:rgba(68, 68, 68, 0.151);} 86 | 87 | 88 | 89 | @media (min-width: 1100px) { 90 | .sidebar { 91 | height: calc(100vh - 60px); 92 | } 93 | .projectBox{ 94 | width:calc((100% / 3) - 30px); 95 | margin:15px 15px; 96 | } 97 | .container { 98 | display:grid; 99 | grid-template-columns: 1fr 4fr; 100 | } 101 | .login{ 102 | display:flex; 103 | align-items: center; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /doc/sprint2/PB.md: -------------------------------------------------------------------------------- 1 | | User Story | Points | Priority | 2 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | 3 | | As a secondary school student, I want to see a timeline so that I can see when a video clip starts and ends. | 8 | High | 4 | | As a secondary school student, I want to split videos in the timeline so that I cut the video into 2 different clips. | 8 | High | 5 | | As a secondary school student, I want to delete videos in the timeline so that I can get rid of the clips I don't want. | 8 | High | 6 | | As a secondary school student, I want to move videos in the timeline so that I can change its postition. | 8 | High | 7 | | As a secondary school student, I want to trim videos in the timeline so that I can change the length of a clip. | 8 | High | 8 | | As a secondary school student, I want to scale videos so that I can resize the video. | 4 | Medium | 9 | | As a secondary school student, I want to position videos so that I can overlap videos. | 4 | Medium | 10 | | As a secondary school student, I want to crop videos so that I can remove unwanted parts from video. | 4 | Medium | 11 | | As a secondary school student, I want to add keyframes to the timeline so that I can create transitions and animations. | 8 | Medium | 12 | | As a secondary school student, I want keyframes to change video properties when the video is played so that transitions and animations are played. | 8 | Medium | 13 | | As a secondary school student, I want to render my project to a video so that I can share my edited videos. | 4 | Medium | 14 | | As a secondary school student, I want to see an estimate of how long rendering will take so that I know how much time I have to wait. | 4 | Medium | 15 | | As a secondary school student, I want to save rendered videos so that I can view them later. | 4 | Medium | 16 | | As a teacher, I want to see my projects so that I can edit them. | 4 | Medium | 17 | | As a teacher, I want to create projects so that I can work on a new project. | 2 | Medium | 18 | | As a teacher, I want to delete projects so that I can remove projects that I don't want. | 2 | Medium | 19 | | As a teacher, I want to edit projects so that I can manipulate my previous work. | 4 | Medium | 20 | | As a teacher, I want to login so that I can save my projects to the cloud. | 8 | Medium | 21 | | As a teacher, I want to upload projects to the cloud so that I can easily transfer my projects to a different device. | 4 | Low | 22 | | As a teacher, I want to download projects from the cloud so that I can easily transfer my projects and work on them. | 4 | Low | 23 | -------------------------------------------------------------------------------- /fire/src/components/actions/actions.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./actions.module.css"; 2 | import { useHistory } from "react-router-dom"; 3 | import { Media, Segment } from "../../model/types"; 4 | import axios from "axios"; 5 | import { GoogleLogout } from "react-google-login"; 6 | import { Link } from "react-router-dom"; 7 | import { useState } from "react"; 8 | 9 | export default function Actions(props: { 10 | projectId: string; 11 | projectUser: string; 12 | mediaList: Media[]; 13 | trackList: Segment[][]; 14 | setProjectUser: (user:string) => void; 15 | }) { 16 | const history = useHistory(); 17 | 18 | const [loadStatus, setLoadStatus] = useState("down"); 19 | 20 | const saveProject = () => { 21 | setLoadStatus("loading"); 22 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 23 | let data = { 24 | projectId: props.projectId, 25 | projectUser: props.projectUser, 26 | mediaList: props.mediaList, 27 | trackList: props.trackList, 28 | }; 29 | instance.put("/saveProject", data, { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` }}).then((res) => { 30 | if (res.status === 200) { 31 | setLoadStatus("success"); 32 | console.log(res.data.success); 33 | } else { 34 | setLoadStatus(res.data.error); 35 | console.log(res.data.error); 36 | } 37 | }); 38 | }; 39 | 40 | let color='rgb(51, 51, 51)'; 41 | let message='saving your project...'; 42 | 43 | if(loadStatus === "loading"){ 44 | color='rgb(51, 51, 51)'; 45 | message='saving your project...'; 46 | } 47 | else if(loadStatus === "success"){ 48 | color='rgb(0, 153, 51)'; 49 | message='Project saved successfully!'; 50 | } 51 | else{ 52 | color='rgb(204, 0, 0)'; 53 | message=loadStatus; 54 | } 55 | 56 | 57 | return ( 58 | 59 |
60 | 61 |
65 | 68 | {message} 69 |
70 |
71 | 72 | 73 | 76 | 77 | ( 80 | 88 | )} 89 | buttonText="Logout" 90 | onLogoutSuccess={() => {props.setProjectUser(""); history.push("/")}} 91 | > 92 | 93 | 96 | 97 | 100 | 107 | 110 |
111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /fire/src/components/mediaPool/mediaPool.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./mediaPool.module.css"; 2 | import React, { useState } from 'react'; 3 | import { Media } from "../../model/types"; 4 | import { Droppable, Draggable } from 'react-beautiful-dnd'; 5 | 6 | const options = { 7 | types: [ 8 | { 9 | accept: { 10 | 'videos/*': ['.mp4', '.mov', '.wmv', '.avi', '.flv'], 11 | 'images/*': ['.jpg', '.png', '.gif', '.jpeg'] 12 | } 13 | }, 14 | ], 15 | multiple: true, 16 | excludeAcceptAllOption: true 17 | }; 18 | 19 | export default function MediaPool(props: any) { 20 | const [status, setStatus] = useState(''); 21 | const [draggedOn, setDraggedOn] = useState(""); 22 | 23 | const listItems = props.mediaList.map((item: Media, index: number) => { 24 | return ( 25 | 26 | {(provided) => ( 27 |
  • 31 | {item.file.name} 32 |

    {item.file.name}

    33 | 36 |
  • 37 | )} 38 |
    39 | ); 40 | }); 41 | 42 | const onClick = async () => { 43 | try { 44 | const files: File[] = []; 45 | //@ts-ignore 46 | const Handle = await window.showOpenFilePicker(options); 47 | setStatus('Loading...'); 48 | for (const entry of Handle) { 49 | let file = await entry.getFile(); 50 | files.push(file); 51 | } 52 | await props.addVideo(files); 53 | setStatus(''); 54 | } catch (error) { 55 | console.log(error); 56 | } 57 | } 58 | 59 | const onDrag = async (e: React.DragEvent) => { 60 | e.preventDefault(); 61 | e.stopPropagation(); 62 | setDraggedOn(""); 63 | if (!e.dataTransfer) return; 64 | const files: File[] = []; 65 | 66 | for (const item of Object.values(e.dataTransfer.items)) { 67 | const file = item.getAsFile(); 68 | 69 | if (file !== null && (file.type.includes('video/') || file.type.includes('image/'))) files.push(file); 70 | else alert(`Could not upload file: ${file?.name}. Only upload videos or images.`); 71 | } 72 | await props.addVideo(files); 73 | setStatus(''); 74 | } 75 | 76 | return ( 77 |
    { e.stopPropagation(); e.preventDefault(); setDraggedOn('draggedOn'); }} 79 | onDragEnter={(e) => { e.stopPropagation(); e.preventDefault(); setDraggedOn('draggedOn'); }} 80 | onDragLeave={(e) => { e.stopPropagation(); e.preventDefault(); setDraggedOn(""); }} 81 | onDrop={onDrag} 82 | className={`${styles.container} ${draggedOn}`} 83 | > 84 |
    85 |

    Project Files

    86 | 93 |
    94 |
    95 | 96 | { 97 | (provided) => ( 98 |
      99 | {listItems} 100 | {provided.placeholder} 101 |
    102 | )} 103 |
    104 |
    105 | 106 |

    {status}

    107 |
    108 | ) 109 | } -------------------------------------------------------------------------------- /doc/sprint0/PB.md: -------------------------------------------------------------------------------- 1 | | User Story | Points | Priority | 2 | | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | 3 | | As a teacher, I want to select videos from my computer on the website so they can be edited. | 8 | High | 4 | | As a teacher, I want to see a live unaltered preview of a video I select from the media pool so that I can easily identify the video. | 8 | High | 5 | | As a teacher, I want to see a live preview of rendered project so that I can visualize the adjustments that have been made. | 8 | High | 6 | | As a teacher, I want to see a loading indicator for videos that are loading so that I know the editor is working on it. | 8 | High | 7 | | As a teacher, I want to see playback controls for videos so that I can easily view the video. | 8 | High | 8 | | As a teacher, I want to see an About page so that I see contact information and version number. | 1 | Low | 9 | | As a secondary school student, I want to see a timeline so that I can see when a video clip starts and ends. | 8 | High | 10 | | As a secondary school student, I want to split videos in the timeline so that I cut the video into 2 different clips. | 8 | High | 11 | | As a secondary school student, I want to delete videos in the timeline so that I can get rid of the clips I don't want. | 8 | High | 12 | | As a secondary school student, I want to move videos in the timeline so that I can change its postition. | 8 | High | 13 | | As a secondary school student, I want to trim videos in the timeline so that I can change the length of a clip. | 8 | High | 14 | | As a secondary school student, I want to scale videos so that I can resize the video. | 4 | Medium | 15 | | As a secondary school student, I want to position videos so that I can overlap videos. | 4 | Medium | 16 | | As a secondary school student, I want to crop videos so that I can remove unwanted parts from video. | 4 | Medium | 17 | | As a secondary school student, I want to add keyframes to the timeline so that I can create transitions and animations. | 8 | Medium | 18 | | As a secondary school student, I want keyframes to change video properties when the video is played so that transitions and animations are played. | 8 | Medium | 19 | | As a secondary school student, I want to render my project to a video so that I can share my edited videos. | 4 | Medium | 20 | | As a secondary school student, I want to see an estimate of how long rendering will take so that I know how much time I have to wait. | 4 | Medium | 21 | | As a secondary school student, I want to save rendered videos so that I can view them later. | 4 | Medium | 22 | | As a teacher, I want to see my projects so that I can edit them. | 4 | Medium | 23 | | As a teacher, I want to create projects so that I can work on a new project. | 2 | Medium | 24 | | As a teacher, I want to delete projects so that I can remove projects that I don't want. | 2 | Medium | 25 | | As a teacher, I want to edit projects so that I can manipulate my previous work. | 4 | Medium | 26 | | As a teacher, I want to login so that I can save my projects to the cloud. | 8 | Medium | 27 | | As a teacher, I want to upload projects to the cloud so that I can easily transfer my projects to a different device. | 4 | Low | 28 | | As a teacher, I want to download projects from the cloud so that I can easily transfer my projects and work on them. | 4 | Low | 29 | -------------------------------------------------------------------------------- /fire/src/routes/editor.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./editor.module.css"; 2 | import MediaPool from "../components/mediaPool/mediaPool"; 3 | import Controls from "../components/controls/controls"; 4 | import MediaPlayer from "../components/mediaPlayer/mediaPlayer"; 5 | import Actions from "../components/actions/actions"; 6 | import Timeline from "../components/timeline/timeline"; 7 | import { Media, Segment, SegmentID } from "../model/types"; 8 | import { WebGLRenderer } from "../model/webgl"; 9 | import Properties from "../components/elements/properties"; 10 | import React, { useState } from "react"; 11 | import { DragDropContext } from 'react-beautiful-dnd'; 12 | 13 | export default function Editor(props: { 14 | canvasRef: HTMLCanvasElement, 15 | mediaList: Media[], 16 | setMediaList: (mediaList: Media[]) => void, 17 | trackList: Segment[][], 18 | setTrackList: (segments: Segment[][]) => void, 19 | addVideo: (file: File[]) => void, 20 | deleteVideo: (media: Media) => void, 21 | playVideo: () => void, 22 | pauseVideo: () => void, 23 | projectWidth: number, 24 | projectHeight: number, 25 | renderer: WebGLRenderer, 26 | projectFramerate: number, 27 | projectDuration: number, 28 | isPlaying: boolean, 29 | currentTime: number, 30 | setCurrentTime: (timestamp: number) => void, 31 | dragAndDrop: (media: Media) => void, 32 | selectedSegment: SegmentID | null, 33 | setSelectedSegment: (selected: SegmentID | null) => void, 34 | updateSegment: (id: SegmentID, segment: Segment) => void, 35 | splitVideo: (timestamp: number) => void, 36 | deleteSelectedSegment: () => void, 37 | projectId: string, 38 | setProjectId: (id: string) => void, 39 | projectUser: string, 40 | setProjectUser: (user:string) => void, 41 | }) { 42 | const [scaleFactor, setScaleFactor] = useState(0.1); 43 | 44 | const handleOnDragEnd = (result: any) => { 45 | if (!result.destination) return; 46 | 47 | const { source, destination } = result; 48 | 49 | // for re-ordering files in the media pool 50 | if (source.droppableId === destination.droppableId) { 51 | const items = props.mediaList.slice(); 52 | const [reorderedItem] = items.splice(result.source.index, 1); 53 | items.splice(result.destination.index, 0, reorderedItem); 54 | props.setMediaList(items); 55 | } 56 | else { 57 | props.dragAndDrop(props.mediaList[result.source.index]); 58 | const items = props.mediaList.slice(); 59 | props.setMediaList(items); 60 | } 61 | } 62 | 63 | return ( 64 | 65 |
    66 | 74 | 79 | 91 | 98 | 109 | 116 |
    117 |
    118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"); 2 | var cors = require("cors"); 3 | var app = express(); 4 | var port = 8000; 5 | var db; 6 | const CLIENT_ID = 7 | "956647101334-784vc8rakg2kbaeil4gug1ukefc9vehk.apps.googleusercontent.com"; 8 | const uri = 9 | "mongodb+srv://dbUser:fire@2021@fire.fojp1.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"; 10 | 11 | const { OAuth2Client } = require("google-auth-library"); 12 | const loginClient = new OAuth2Client(CLIENT_ID); 13 | const MongoClient = require("mongodb").MongoClient; 14 | const ObjectID = require("mongodb").ObjectID; 15 | const client = new MongoClient(uri, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true, 18 | }); 19 | 20 | app.use(cors()); 21 | app.use(express.json({limit: '50mb'})); 22 | app.use( 23 | express.urlencoded({ 24 | extended: true, 25 | limit: '50mb', 26 | }) 27 | ); 28 | app.use(express.json()); 29 | 30 | client.connect((err) => { 31 | db = client.db("fire_db"); 32 | console.log("Connected to DB..."); 33 | app.listen(port, () => { 34 | console.log(`App listening at http://localhost:${port}`); 35 | }); 36 | }); 37 | 38 | // project endpoints 39 | app.use("/", async (req, res, next) => { 40 | try { 41 | const ticket = await loginClient.verifyIdToken({ 42 | idToken: req.headers.authorization.split("Bearer ")[1], 43 | audience: CLIENT_ID, 44 | }); 45 | req.email = ticket.getPayload().email; 46 | next(); 47 | } catch (error) { 48 | res.status(401).json({ error: "Unauthorized" }); 49 | } 50 | }); 51 | 52 | app.get("/getEmail", async (req, res) => { 53 | res.status(200).json({ email: req.email }); 54 | }); 55 | 56 | app.get("/getProjects", (req, res) => { 57 | db.collection("projects") 58 | .find({ owner: req.email }) 59 | .toArray((err, response) => { 60 | if (err) { 61 | res 62 | .status(400) 63 | .json({ error: "Unable to get project. Try again later!" }); 64 | } else { 65 | res.status(200).json(response); 66 | } 67 | }); 68 | }); 69 | 70 | app.put("/addProject", (req, res) => { 71 | let data = { 72 | owner: req.email, 73 | name: req.body.name, 74 | framerate: req.body.framerate, 75 | width: req.body.width, 76 | height: req.body.height, 77 | }; 78 | db.collection("projects").insertOne(data, (err, response) => { 79 | if (err) { 80 | res 81 | .status(400) 82 | .json({ error: "Unable to create project. Try again later!" }); 83 | } else if (response.insertedCount === 1) { 84 | res.status(200).json(response.ops[0]); 85 | } else { 86 | res 87 | .status(500) 88 | .json({ error: "Unable to create project. Try again later!" }); 89 | } 90 | }); 91 | }); 92 | 93 | app.put("/editProject", (req, res) => { 94 | let data = { 95 | _id: new ObjectID(req.body._id), 96 | owner: req.email, 97 | name: req.body.name, 98 | framerate: req.body.framerate, 99 | width: req.body.width, 100 | height: req.body.height, 101 | }; 102 | db.collection("projects").findOneAndUpdate( 103 | { _id: new ObjectID(data._id), owner: req.email }, 104 | { $set: data }, 105 | (err, response) => { 106 | if (err) { 107 | res 108 | .status(400) 109 | .json({ error: "Unable to edit project. Please try again!" }); 110 | } else { 111 | res.status(200).json(response); 112 | } 113 | } 114 | ); 115 | }); 116 | 117 | app.put("/saveProject", (req, res) => { 118 | const update = { 119 | $set: { 120 | mediaList: req.body.mediaList, 121 | trackList: req.body.trackList, 122 | }, 123 | }; 124 | const query = { projectId: req.body.projectId }; 125 | const options = { upsert: true }; 126 | 127 | db.collection("projectFiles").updateOne( 128 | query, 129 | update, 130 | options, 131 | (err, response) => { 132 | if (err) { 133 | res 134 | .status(500) 135 | .json({ error: "Unable to save project to cloud. Try again later!" }); 136 | } else if (response.matchedCount === 1 || response.upsertedCount === 1) { 137 | res 138 | .status(200) 139 | .json({ success: "Project has been saved successfully!" }); 140 | } else { 141 | res 142 | .status(500) 143 | .json({ error: "Unable to save project to cloud. Try again later!" }); 144 | } 145 | } 146 | ); 147 | }); 148 | 149 | app.delete("/deleteProject/:id", (req, res) => { 150 | const query = { 151 | _id: new ObjectID(req.params.id), 152 | owner: req.email, 153 | }; 154 | db.collection("projects").deleteOne(query, (err, response) => { 155 | if (err) { 156 | res 157 | .status(400) 158 | .json({ error: "Unable to delete project. Try again later!" }); 159 | } else { 160 | db.collection("projectFiles").deleteOne( 161 | { projectId: req.params.id }, 162 | (err, response) => { 163 | if (err) { 164 | res 165 | .status(400) 166 | .json({ error: "Unable to delete project. Try again later!" }); 167 | } else { 168 | res 169 | .status(200) 170 | .json({ success: "Project has been successfully deleted." }); 171 | } 172 | } 173 | ); 174 | } 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /fire/src/routes/projects.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import styles from "./projects.module.css"; 3 | import axios from "axios"; 4 | import { useState } from "react"; 5 | import { Project } from "../model/types"; 6 | import { useHistory } from "react-router-dom"; 7 | 8 | export default function Projects(props: { projectUser: string, projects: Project[], setProjects: (projects: Project[]) => void }) { 9 | const history = useHistory(); 10 | 11 | function modifyProject(event: any) { 12 | event.preventDefault(); 13 | if (!modifyingProject) return; 14 | 15 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 16 | 17 | if (modifyingProject._id !== "") { 18 | instance.put("/editProject", modifyingProject, { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }).then((res) => { setModifyingProject(null); }); 19 | let newProjects = []; 20 | for (const proj of props.projects) { 21 | if (proj._id === modifyingProject._id) { 22 | newProjects.push(modifyingProject); 23 | } else { 24 | newProjects.push(proj); 25 | } 26 | } 27 | props.setProjects(newProjects); 28 | } else { 29 | instance.put("/addProject", modifyingProject, { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }).then((res) => { 30 | props.setProjects([...props.projects, res.data]); setModifyingProject(null); }); 31 | } 32 | } 33 | 34 | function deleteProject(id: string) { 35 | const instance = axios.create({ baseURL: "http://localhost:8000" }); 36 | instance 37 | .delete(`/deleteProject/${id}`, { headers: { Authorization: `Bearer ${sessionStorage.getItem("token")}` } }) 38 | .then((res) => { 39 | props.setProjects(props.projects.filter(item => item._id !== id)); 40 | }); 41 | } 42 | 43 | const [modifyingProject, setModifyingProject] = useState(null); 44 | 45 | const NewProject = () => { 46 | if (!modifyingProject) return; 47 | 48 | return (
    49 |

    {modifyingProject._id === "" ? "Create" : "Modify"} Project

    50 | 53 |
    54 |
    55 | 56 | { setModifyingProject({ ...modifyingProject, name: event.target.value }) }}> 58 |
    59 | 60 | { setModifyingProject({ ...modifyingProject, framerate: +event.target.value }) }}> 62 |
    63 | 64 | { setModifyingProject({ ...modifyingProject, width: +event.target.value }) }}> 66 |
    67 | 68 | { setModifyingProject({ ...modifyingProject, height: +event.target.value }) }}> 70 |
    71 | 74 |
    75 |
    76 |
    77 |
    78 | ) 79 | }; 80 | 81 | return ( 82 |
    83 |
    84 |
    85 | 86 | 87 | 88 |
    89 |

    Fire

    90 |

    Video Editor

    91 |
    92 |
    93 | 94 | layers Current Projects 95 | 96 | 97 | save_alt Exported Files 98 | 99 |
    100 | 101 |
    102 | {props.projects.map((project, index) => { 103 | return
    { history.push("/editor"); }} 109 | > 110 |
    111 |

    {project.name}

    112 | 115 | 118 |
    119 |
    120 | })} 121 | {modifyingProject != null ? NewProject() : null} 122 |
    123 | 133 |
    134 | ); 135 | } 136 | -------------------------------------------------------------------------------- /doc/sprint1/Sprint1_marking.txt: -------------------------------------------------------------------------------- 1 | Team Name: thenerds 2 | 3 | Planning Meetings (RPM.md, sprint1.md) (Max 5 marks) 4 | -RPM.md (Release Planning Meeting) 5 | [3 marks=Meeting is documented, participants are recorded, everyone has participated, Release goals are specified and there are sufficient references to user stories to be completed during the release] 6 | [2 marks=Meeting is documented, participants are not recorded or at least one member has not participated, Release goals are somewhat specified and there are some references to user stories to be completed during the release] 7 | [1 mark=Meeting is documented, participants are not recorded or the majority has not participated, Release goals are unclear or the references to user stories to be completed during the release are missing or incomplete] 8 | [0 marks=No meeting document submitted or the submitted document contains no relevant information to the RPM] 9 | Comment: Detail for the RPM release goal is missing 10 | -Sprint Planning meeting (sprint1.md) 11 | [2 marks=Meeting and sprint goal is documented, all spikes clearly identified, team capacity recorded, participants are recorded, everyone has participated, 12 | decisions about user stories to be completed this sprint are clear, tasks breakdown is done.] 13 | [1 mark=Meeting and sprint goal is documented, some spikes not clearly identified, one or more members of team's capacity not recorded, participants are not recorded or the majority has not participated, decisions are unclear, 14 | there is no clear evidence which user story will be completed or task breakdown is unclear or incomplete] 15 | [0 marks=No meeting document submitted or the submitted document contains no relevant information to the sprint 1] 16 | 17 | Mark: 3 18 | 19 | Daily Stand-ups (on Discord 301 server within your team category called #standup) (Max 10 marks, 2 marks per daily standup) 20 | - Team updates each other on what they have done in their #standup Microsoft Teams channel 21 | - 5 standups per sprint per person is required 22 | [2 marks=all teams members have sent their updates in the channel] 23 | [1 mark=atleast 1 group member has not sent their update] 24 | [0 marks=atleast 2 group members or more have not sent their updates] 25 | 26 | Mark: 10 27 | 28 | User Stories (Tracked in Jira) (Max 10 marks) 29 | -Correctly formulated user stories 30 | [2 marks=all stories in Jira Backlog follow the User Story template] 31 | [1 mark=two or more stories in Jira Backlog do not follow the template] 32 | [0 marks=the majority of stories in Jira Backlog do not follow the template or no stories at all] 33 | -Broken down into tasks 34 | [2 marks=all Sprint 1 stories estimated, broken into tasks, tasks assigned to all team members] 35 | [1 mark=one or more Sprint 1 stories are not recorded Jira, or not estimated, or nor broken into tasks, or at least one task not assigned, or at least one team member unassigned a task] 36 | [0 marks=the majority of Sprint 1 stories not recorded in Jira or not estimated, or nor broken into tasks, or tasks not assigned, or team members unassigned a task ] 37 | -Estimated 38 | [2 marks=all stories in Jira Backlog are point estimated and the point value is recorded in Jira] 39 | [1 mark=two or more stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 40 | [0 marks=the majority of stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 41 | -Prioritized 42 | [2 marks=all stories in Jira Backlog are prioritized in the order which matches the priority of their associated milestones] 43 | [1 mark=two or more stories in Jira Backlog not are prioritized in the order which matches the priority of their associated milestones] 44 | [0 marks=the majority of stories in Jira Backlog appear to be not prioritized in the order which matches the priority of their associated milestones] 45 | -Clearly Defined Acceptance Criteria 46 | [2 marks=all Sprint 1 stories have clearly defined and testable acceptance criteria] 47 | [1 mark=one or more Sprint 1 stories do not have clearly defined or testable acceptance criteria] 48 | [0 marks=the majority of the Sprint1 stories do not have clearly defined or testable acceptance criteria] 49 | 50 | Mark: 10 51 | 52 | Tracking on Jira: (Max 20 marks) 53 | - Are all your stories and tasks being worked in this sprint tracked on Active Sprint Board? 54 | [0=no stories on Active Sprint Board] 55 | [1=part of the stories missing] 56 | [2=all stories recorded] 57 | - Did you start all stories for sprint one? 58 | [0=no story started] 59 | [1=part of the stories not started and explanation why not provided] 60 | [2=all stories started or some stories not started, but logical explanation provided as a story comment] 61 | - Do the pull request on GitHub match the tasks listed on Jira Active Sprint Board? 62 | [0=no tasks or tasks do not match at all] 63 | [1=two or more tasks are missing] 64 | [2=no tasks are missing] 65 | - Is the pull request done by the person to whom the task has been assigned to? 66 | [0=no task was completed by anyone] 67 | [1=two or more tasks were not completed as assigned] 68 | [2=all assigned tasks are completed] 69 | - Is each pull request made from a separate branch for each individual task? 70 | [0=pull request for the majority of tasks not made from own separate branch] 71 | [1=pull request for 4 or more tasks not made from own separate branch] 72 | [2=pull request for 3 or more tasks not made from own separate branch] 73 | [3=pull request for 2 or more tasks not made from own separate branch] 74 | [4=pull request for 1 or more tasks not made from own separate branch] 75 | [5=pull request for each task made from own separate branch] 76 | - Is the git commit message clear? 77 | [0=majority of commit message do not begin with the ticket number of the task followed by a short description of commit changes] 78 | [1=one or more commit messages do not begin with the ticket number of the task followed by a short description of commit changes] 79 | [2=all commit message begin with the ticket number of the task followed by a short description of commit changes] 80 | - Tasks Distribution 81 | [0=most of team members have no tasks assigned and/or completed] 82 | [1=at least one team member did not complete any task or did not have any task assigned] 83 | [2=every team member had at least one task assigned and made a pull request] 84 | [3=every team member had at least one task assigned and completed] 85 | - Branch Naming 86 | [0=most branch names are not named after the associated single ticket number] 87 | [1=at least one branch is not named after the associated single ticket number] 88 | [2=every team branch is named after the associated single ticket number] 89 | 90 | Mark: 20 91 | 92 | Sprint Completion: (Max 20 marks) 93 | - With A Minimum Of 5 Stories 94 | [x = Percentage of story points completed * 20] 95 | 96 | Mark: 20 97 | 98 | System Design - (PDF or MD file or another suitable format) (Max 10 marks) 99 | - Cover Page and Table of Contents 100 | [2 marks: both are present] 101 | [1 mark: at least one is present] 102 | [0 marks: None present] 103 | 104 | - CRC Cards [or equivalent, if the team is not using CRC) 105 | [4 marks: Class names and Collaborators have matching names and responsibilities are stated clearly] 106 | [3 marks: At least one of the class names does not match the collaborator names or the responsibilities for at least one class are unclear] 107 | [2 marks: Two class names do not match the collaborator names or the responsibilities of two or more classes are not stated or are unclear] 108 | [1 mark: The majority of class names do not match the collaborator names or the responsibilities of the majority of the classes are not stated or are unclear] 109 | [0 marks: No CRC provided or the provided document does not match the CRC model] 110 | 111 | - Software Architecture Diagram 112 | [4 marks: The Architecture Diagram is present, it is formatted using proper graphic symbols, and it follows a known Architecture diagram. 113 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 114 | [3 marks: The Architecture Diagram is present, it is not formatted well, and it follows somewhat a known Architecture diagram. 115 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 116 | [2 marks: The Architecture Diagram is present, it is not formatted well, or it does not follow a known Architecture diagram. 117 | A web link or reference has been provided, but it does not support the used architecture] 118 | [1 mark: The Architecture Diagram is present, it is not formatted well, and it is unclear what Architecture it follows. 119 | There is no web link or other reference lending support to the presented diagram. 120 | [0 marks: No diagram present or the presented document does not look like a software architecture diagram] 121 | 122 | Mark: 10 123 | 124 | Total: 73/75 -------------------------------------------------------------------------------- /fire/src/model/playbackController.tsx: -------------------------------------------------------------------------------- 1 | import Editor from "../routes/editor"; 2 | import { Media, Project, Segment, SegmentID, Source } from "./types"; 3 | import React, { useEffect, useRef, useState } from "react"; 4 | import { WebGLRenderer } from "./webgl"; 5 | import { Route, BrowserRouter as Router, Switch, Redirect } from "react-router-dom"; 6 | import About from "../routes/about"; 7 | import ExportPage from "../routes/exportPage"; 8 | import Login from "../routes/login"; 9 | import Projects from "../routes/projects"; 10 | 11 | export default function PlaybackController(props: { 12 | setProjects: (projects: Project[]) => void; 13 | projects: Project[]; 14 | projectUser: string; 15 | setProjectUser: (user: string) => void; 16 | canvasRef: HTMLCanvasElement; 17 | mediaList: Media[]; 18 | setMediaList: (mediaList: Media[]) => void; 19 | trackList: Segment[][]; 20 | setTrackList: (segments: Segment[][]) => void; 21 | addVideo: (file: File[]) => void; 22 | deleteVideo: (media: Media) => void; 23 | renderer: WebGLRenderer; 24 | dragAndDrop: (media: Media) => void; 25 | setSelectedSegment: (selected: SegmentID | null) => void; 26 | selectedSegment: SegmentID | null; 27 | updateSegment: (id: SegmentID, segment: Segment) => void; 28 | splitVideo: (timestamp: number) => void; 29 | deleteSelectedSegment: () => void; 30 | projectWidth: number; 31 | projectHeight: number; 32 | projectFramerate: number; 33 | projectDuration: number; 34 | projectId: string; 35 | setProjectId: (id: string) => void; 36 | setProjectDuration: (duration: number) => void; 37 | }) { 38 | const [isPlaying, setIsPlaying] = useState(false); 39 | const isRecordingRef = useRef(false); 40 | const [currentTime, _setCurrentTime] = useState(0); 41 | const trackListRef = useRef(props.trackList); 42 | const playbackStartTimeRef = useRef(0); 43 | const lastPlaybackTimeRef = useRef(0); 44 | const projectDurationRef = useRef(0); 45 | const mediaListRef = useRef([]); 46 | const isPlayingRef = useRef(false); 47 | const SKIP_THREASHOLD = 100; 48 | let recordedChunks: Array; 49 | const mediaRecorderRef = useRef(); 50 | 51 | trackListRef.current = props.trackList; 52 | projectDurationRef.current = props.projectDuration; 53 | mediaListRef.current = props.mediaList; 54 | isPlayingRef.current = isPlaying; 55 | 56 | const setCurrentTime = (timestamp: number) => { 57 | lastPlaybackTimeRef.current = timestamp; 58 | playbackStartTimeRef.current = performance.now(); 59 | _setCurrentTime(timestamp); 60 | if (!isPlayingRef.current) renderFrame(false); 61 | }; 62 | 63 | useEffect(() => { 64 | if (!isPlayingRef.current) renderFrame(false); 65 | }, [props.trackList]); 66 | 67 | useEffect(() => { 68 | if (currentTime > props.projectDuration) 69 | setCurrentTime(props.projectDuration); 70 | }, [props.projectDuration]); 71 | 72 | const renderFrame = async (update: boolean) => { 73 | let curTime = 74 | performance.now() - 75 | playbackStartTimeRef.current + 76 | lastPlaybackTimeRef.current; 77 | if (!update) curTime = lastPlaybackTimeRef.current; 78 | if (curTime >= projectDurationRef.current) 79 | curTime = projectDurationRef.current; 80 | _setCurrentTime(curTime); 81 | 82 | for (const media of mediaListRef.current) { 83 | for (const source of media.sources) { 84 | source.inUse = false; 85 | } 86 | } 87 | 88 | let segments: Segment[] = []; 89 | let elements: HTMLVideoElement[] = []; 90 | let needsSeek = false; 91 | 92 | for (let i = trackListRef.current.length - 1; i >= 0; i--) { 93 | for (let j = 0; j < trackListRef.current[i].length; j++) { 94 | const segment = trackListRef.current[i][j]; 95 | if ( 96 | curTime >= segment.start && 97 | curTime < segment.start + segment.duration 98 | ) { 99 | let source = segment.media.sources.find( 100 | (source) => source.track === i 101 | ) as Source; 102 | source.inUse = true; 103 | let mediaTime = curTime - segment.start + segment.mediaStart; 104 | if ( 105 | Math.abs(source.element.currentTime * 1000 - mediaTime) > 106 | SKIP_THREASHOLD || 107 | source.element.paused 108 | ) 109 | needsSeek = true; 110 | segments.push(segment); 111 | elements.push(source.element); 112 | } 113 | } 114 | } 115 | 116 | for (const media of mediaListRef.current) { 117 | for (const source of media.sources) { 118 | if (!source.inUse) { 119 | source.element.pause(); 120 | source.inUse = true; 121 | } 122 | } 123 | } 124 | 125 | if (needsSeek) { 126 | if (isRecordingRef.current) { 127 | if (mediaRecorderRef.current != null) mediaRecorderRef.current.pause(); 128 | } 129 | for (let i = 0; i < segments.length; i++) { 130 | const segment = segments[i]; 131 | 132 | elements[i].pause(); 133 | let mediaTime = (curTime - segment.start + segment.mediaStart) / 1000; 134 | 135 | if (elements[i].currentTime !== mediaTime) { 136 | await new Promise((resolve, reject) => { 137 | elements[i].onseeked = () => resolve(); 138 | elements[i].currentTime = mediaTime; 139 | }); 140 | } 141 | } 142 | try { 143 | await Promise.allSettled(elements.map((element) => element.play())); 144 | } catch (error) { } 145 | lastPlaybackTimeRef.current = curTime; 146 | playbackStartTimeRef.current = performance.now(); 147 | if (isRecordingRef.current) { 148 | if (mediaRecorderRef.current != null) mediaRecorderRef.current.resume(); 149 | } 150 | } 151 | 152 | props.renderer.drawSegments(segments, elements, curTime); 153 | 154 | if (!isPlayingRef.current) { 155 | for (const element of elements) { 156 | element.pause(); 157 | } 158 | return; 159 | } 160 | 161 | if (curTime === projectDurationRef.current) { 162 | pause(); 163 | if (isRecordingRef.current) { 164 | if (mediaRecorderRef.current != null) mediaRecorderRef.current.stop(); 165 | isRecordingRef.current = false; 166 | } 167 | return; 168 | } 169 | 170 | (setTimeout(() => { 171 | renderFrame(true); 172 | }, 1 / props.projectFramerate) as unknown) as number; 173 | }; 174 | 175 | const play = async () => { 176 | if (currentTime >= projectDurationRef.current) return; 177 | 178 | setIsPlaying(true); 179 | lastPlaybackTimeRef.current = currentTime; 180 | playbackStartTimeRef.current = performance.now(); 181 | isPlayingRef.current = true; 182 | 183 | renderFrame(true); 184 | }; 185 | 186 | const pause = () => { 187 | setIsPlaying(false); 188 | }; 189 | 190 | function Render() { 191 | let canvas: HTMLCanvasElement | null = props.canvasRef; 192 | 193 | // Optional frames per second argument. 194 | if (canvas != null) { 195 | let stream = canvas.captureStream(props.projectFramerate); 196 | recordedChunks = []; 197 | let options = { mimeType: "video/webm; codecs=vp9" }; 198 | mediaRecorderRef.current = new MediaRecorder(stream, options); 199 | mediaRecorderRef.current.ondataavailable = handleDataAvailable; 200 | mediaRecorderRef.current.onstop = download; 201 | setCurrentTime(0); 202 | isRecordingRef.current = true; 203 | mediaRecorderRef.current.start(); 204 | setIsPlaying(true); 205 | renderFrame(true); 206 | } 207 | } 208 | 209 | function handleDataAvailable(event: any) { 210 | if (event.data.size > 0) { 211 | recordedChunks.push(event.data); 212 | } 213 | } 214 | 215 | function download() { 216 | var blob = new Blob(recordedChunks, { 217 | type: "video/webm", 218 | }); 219 | var url = URL.createObjectURL(blob); 220 | var a = document.createElement("a"); 221 | document.body.appendChild(a); 222 | a.href = url; 223 | a.download = "test.webm"; 224 | a.click(); 225 | window.URL.revokeObjectURL(url); 226 | } 227 | 228 | return ( 229 | 230 | 231 | 232 | 240 | 241 | 242 | 243 | 244 | 245 | {props.projectUser != "" ? : } 250 | 251 | 252 | {props.projectUser != "" ? : } 260 | 261 | 262 | {props.projectUser === "" ? 265 | : } 266 | 267 | 268 | 269 | ); 270 | } 271 | -------------------------------------------------------------------------------- /doc/sprint3/sprint3-marks.txt: -------------------------------------------------------------------------------- 1 | Team Name: The Nerds 2 | 3 | (Convention: N=sprint number, ex: sprintN.md means sprint2.md for sprint 2, and SR(N-1)md means SR1.md for sprint 2, that is Sprint Retrospective for Sprint 1, to be hold during planning meeting for Sprint 2) 4 | 5 | Planning Meetings (SR2.md, sprint3.md) (Max 4 marks) (4/4) 6 | 7 | -SR2.md (Sprint Retrospective) 8 | [2 marks=Meeting is documented, participants are recorded, everyone has participated, analysis of good practices/bad practices wad made.] 9 | [1 mark=Meeting is documented, participants are not recorded or the majority has not participated, not enough analysis has been done] 10 | [0 marks=No meeting document submitted or the submitted document contains no relevant information] 11 | 12 | 2/2 13 | 14 | -Sprint Planning meeting (sprint3.md) 15 | [2 marks=Meeting and sprint goal is documented, all spikes clearly identified, team capacity recorded, participants are recorded, everyone has participated, 16 | decisions about user stories to be completed this sprint are clear, tasks breakdown is done.] 17 | [1 mark=Meeting and sprint goal is documented, some spikes not clearly identified, one or more members of team's capacity not recorded, participants are not recorded or the majority has not participated, decisions are unclear, 18 | there is no clear evidence which user story will be completed or task breakdown is unclear or incomplete] 19 | [0 marks=No meeting document submitted or the submitted document contains no relevant information to the sprint 1] 20 | 21 | 2/2 22 | 23 | Daily Stand-ups (on Discord 301 server within your team category called #standup) (Max 10 marks, 2 marks per daily standup) 24 | - Team updates each other on what they have done in their #standup Microsoft Teams channel 25 | - 5 standups per sprint required 26 | [2 marks=all teams members have sent their updates in the channel] 27 | [1 mark=atleast 1 group member has not sent their update] 28 | [0 marks=atleast 2 group members or more have not sent their updates] 29 | 30 | 10/10 31 | 32 | User Stories (Tracked in Jira) (Max 10 marks) (8/10) 33 | -Correctly formulated user stories 34 | [2 marks=all stories in Jira Backlog follow the User Story template] 35 | [1 mark=two or more stories in Jira Backlog do not follow the template] 36 | [0 marks=the majority of stories in Jira Backlog do not follow the template or no stories at all] 37 | 38 | 2/2 39 | 40 | -Broken down into tasks 41 | [2 marks=all Sprint 1 stories estimated, broken into tasks, tasks assigned to all team members] 42 | [1 mark=one or more Sprint 1 stories are not recorded Jira, or not estimated, or nor broken into tasks, or at least one task not assigned, or at least one team member unassigned a task] 43 | [0 marks=the majority of Sprint 1 stories not recorded in Jira or not estimated, or nor broken into tasks, or tasks not assigned, or team members unassigned a task ] 44 | 45 | 2/2 46 | 47 | -Estimated 48 | [2 marks=all stories in Jira Backlog are point estimated and the point value is recorded in Jira] 49 | [1 mark=two or more stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 50 | [0 marks=the majority of stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 51 | 52 | 2/2 53 | 54 | -Prioritized 55 | [2 marks=all stories in Jira Backlog are prioritized in the order which matches the priority of their associated milestones] 56 | [1 mark=two or more stories in Jira Backlog not are prioritized in the order which matches the priority of their associated milestones] 57 | [0 marks=the majority of stories in Jira Backlog appear to be not prioritized in the order which matches the priority of their associated milestones] 58 | 59 | 2/2 60 | 61 | -Clearly Defined Acceptance Criteria 62 | [2 marks=all Sprint 1 stories have clearly defined and testable acceptance criteria] 63 | [1 mark=one or more Sprint 1 stories do not have clearly defined or testable acceptance criteria] 64 | [0 marks=the majority of the Sprint1 stories do not have clearly defined or testable acceptance criteria] 65 | 66 | 2/2 67 | 68 | Tracking on Jira: (Max 24 marks) (24/24) 69 | - Are all your stories and tasks being worked in this sprint tracked on Active Sprint Board? 70 | [0=no stories on Active Sprint Board] 71 | [1=part of the stories missing] 72 | [2=all stories recorded] 73 | 74 | 2/2 75 | 76 | - Did you start all stories for sprint Three? 77 | [0=no story started] 78 | [1=part of the stories not started and explanation why not provided] 79 | [2=all stories started or some stories not started, but logical explanation provided as a story comment] 80 | 81 | 2/2 82 | 83 | - Do the pull request on GitHub match the tasks listed on Jira Active Sprint Board? 84 | [0=no tasks or tasks do not match at all] 85 | [1=two or more tasks are missing] 86 | [2=no tasks are missing] 87 | 88 | 2/2 89 | 90 | - Is the pull request done by the person to whom the task has been assigned to? 91 | [0=no task was completed by anyone] 92 | [1=two or more tasks were not completed as assigned] 93 | [2=all assigned tasks are completed] 94 | 95 | 2/2 96 | 97 | - Is each pull request made from a separate branch for each individual task? 98 | [0=pull request for the majority of tasks not made from own separate branch] 99 | [1=pull request for 4 or more tasks not made from own separate branch] 100 | [2=pull request for 3 or more tasks not made from own separate branch] 101 | [3=pull request for 2 or more tasks not made from own separate branch] 102 | [4=pull request for 1 or more tasks not made from own separate branch] 103 | [5=pull request for each task made from own separate branch] 104 | 105 | 5/5 106 | 107 | - Is the git commit message clear? 108 | [0=majority of commit message do not begin with the ticket number of the task followed by a short description of commit changes] 109 | [1=one or more commit messages do not begin with the ticket number of the task followed by a short description of commit changes] 110 | [2=all commit message begin with the ticket number of the task followed by a short description of commit changes] 111 | 112 | 2/2 113 | 114 | - Tasks Distribution 115 | [0=most of team members have no tasks assigned and/or completed] 116 | [1=at least one team member did not complete any task or did not have any task assigned] 117 | [2=every team member had at least one task assigned and made a pull request] 118 | [3=every team member had at least one task assigned and completed] 119 | 120 | 3/3 121 | 122 | - Branch Naming 123 | [0=most branch names are not named after the associated single ticket number] 124 | [1=at least one branch is not named after the associated single ticket number] 125 | [2=every team branch is named after the associated single ticket number] 126 | 127 | 2/2 128 | 129 | -Burndown chart 130 | [0=document not available or not containing a valid burndown chart] 131 | [1=burndown chart present, matches Jira chart, but no explanations provided, or explanations are not adequate.] 132 | [2=burndown chart present, matched jira chart, and explantions are adequate.] 133 | 134 | 2/2 135 | 136 | -Schedule 137 | [0=document not available or not containing a valid diagram] 138 | [1=diagram present, but no critical path or explanations provided, or explanations are not adequate.] 139 | [2=diagram present, critical path computed, and explantions are adequate.] 140 | 141 | 2/2 142 | 143 | Sprint Completion: (Max 20 marks) 144 | - With A Minimum Of 5 Stories 145 | [x = Percentage of story points completed * 20] 146 | 147 | 20/20 148 | 149 | System Design - (PDF or MD file or another suitable format) (Max 10 marks) (10/10) 150 | - Cover Page and Table of Contents 151 | [2 marks: both are present] 152 | [1 mark: at least one is present] 153 | [0 marks: None present] 154 | 155 | 2/2 156 | 157 | - CRC Cards [or equivalent, if the team is not using CRC) 158 | [4 marks: Class names and Collaborators have matching names and responsibilities are stated clearly] 159 | [3 marks: At least one of the class names does not match the collaborator names or the responsibilities for at least one class are unclear] 160 | [2 marks: Two class names do not match the collaborator names or the responsibilities of two or more classes are not stated or are unclear] 161 | [1 mark: The majority of class names do not match the collaborator names or the responsibilities of the majority of the classes are not stated or are unclear] 162 | [0 marks: No CRC provided or the provided document does not match the CRC model] 163 | 164 | 4/4 165 | 166 | - Software Architecture Diagram 167 | [4 marks: The Architecture Diagram is present, it is formatted using proper graphic symbols, and it follows a known Architecture diagram. 168 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 169 | [3 marks: The Architecture Diagram is present, it is not formatted well, and it follows somewhat a known Architecture diagram. 170 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 171 | [2 marks: The Architecture Diagram is present, it is not formatted well, or it does not follow a known Architecture diagram. 172 | A web link or reference has been provided, but it does not support the used architecture] 173 | [1 mark: The Architecture Diagram is present, it is not formatted well, and it is unclear what Architecture it follows. 174 | There is no web link or other reference lending support to the presented diagram. 175 | [0 marks: No diagram present or the presented document does not look like a software architecture diagram] 176 | 177 | 4/4 178 | 179 | Total: 78/78 180 | 181 | -------------------------------------------------------------------------------- /fire/src/model/webgl.ts: -------------------------------------------------------------------------------- 1 | import { m4 } from "twgl.js"; 2 | import { calculateProperties } from "../utils/utils"; 3 | import { FRAGMENT_SHADER, VERTEX_SHADER } from "./shaders"; 4 | import { KeyFrame, Segment } from "./types"; 5 | 6 | 7 | export class WebGLRenderer { 8 | context: WebGLRenderingContext; 9 | program: WebGLProgram; 10 | 11 | positionLocation: number; 12 | texcoordLocation: number; 13 | 14 | matrixLocation: WebGLUniformLocation; 15 | textureLocation: WebGLUniformLocation; 16 | 17 | positionBuffer: WebGLBuffer; 18 | texcoordBuffer: WebGLBuffer; 19 | 20 | constructor(public canvas: HTMLCanvasElement, public projectWidth: number, public projectHeight: number) { 21 | this.context = canvas.getContext("webgl") as WebGLRenderingContext; 22 | if (!this.context) console.error("Failed to get webgl context!"); 23 | 24 | // setup GLSL program 25 | this.program = this.context.createProgram() as WebGLProgram; 26 | 27 | this.context.attachShader(this.program, this.loadShader(VERTEX_SHADER, this.context.VERTEX_SHADER) as WebGLShader); 28 | this.context.attachShader(this.program, this.loadShader(FRAGMENT_SHADER, this.context.FRAGMENT_SHADER) as WebGLShader); 29 | 30 | this.context.linkProgram(this.program); 31 | const linked = this.context.getProgramParameter(this.program, this.context.LINK_STATUS); 32 | 33 | if (!linked) { 34 | console.error("Error in program linking:" + this.context.getProgramInfoLog(this.program)); 35 | this.context.deleteProgram(this.program); 36 | } 37 | 38 | this.context.useProgram(this.program); 39 | 40 | // look up where the vertex data needs to go. 41 | this.positionLocation = this.context.getAttribLocation(this.program, "a_position"); 42 | this.texcoordLocation = this.context.getAttribLocation(this.program, "a_texcoord"); 43 | 44 | // lookup uniforms 45 | this.matrixLocation = this.context.getUniformLocation(this.program, "u_matrix") as WebGLUniformLocation; 46 | this.textureLocation = this.context.getUniformLocation(this.program, "u_texture") as WebGLUniformLocation; 47 | 48 | // Create a buffer to put three 2d clip space points in 49 | this.positionBuffer = this.context.createBuffer() as WebGLBuffer; 50 | this.context.bindBuffer(this.context.ARRAY_BUFFER, this.positionBuffer); 51 | // Put a unit quad in the buffer 52 | let positions = [ 53 | 0, 0, 54 | 1, 1, 55 | 1, 0, 56 | 0, 0, 57 | 0, 1, 58 | 1, 1, 59 | ] 60 | this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(positions), this.context.STATIC_DRAW); 61 | 62 | // Create a buffer for texture coords 63 | this.texcoordBuffer = this.context.createBuffer() as WebGLBuffer; 64 | this.context.bindBuffer(this.context.ARRAY_BUFFER, this.texcoordBuffer); 65 | 66 | // Put texcoords in the buffer 67 | let texcoords = [ 68 | 0, 0, 69 | 1, 1, 70 | 1, 0, 71 | 0, 0, 72 | 0, 1, 73 | 1, 1, 74 | ] 75 | this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(texcoords), this.context.STATIC_DRAW); 76 | } 77 | 78 | public drawSegments(segments: Segment[], elements: HTMLVideoElement[], timestamp: number) { 79 | // Tell WebGL how to convert from clip space to pixels 80 | this.context.viewport(0, 0, this.projectWidth, this.projectHeight); 81 | this.context.clear(this.context.COLOR_BUFFER_BIT); 82 | 83 | for (let i = 0; i < segments.length; i++) { 84 | this.drawImage(segments[i], elements[i], calculateProperties(segments[i], timestamp)); 85 | } 86 | 87 | this.context.flush(); 88 | } 89 | 90 | private loadShader(shaderSource: string, shaderType: number) { 91 | // Create the shader object 92 | const shader = this.context.createShader(shaderType) as WebGLShader; 93 | 94 | // Load the shader source 95 | this.context.shaderSource(shader, shaderSource); 96 | 97 | // Compile the shader 98 | this.context.compileShader(shader); 99 | 100 | // Check the compile status 101 | const compiled = this.context.getShaderParameter(shader, this.context.COMPILE_STATUS); 102 | 103 | if (!compiled) { 104 | // Something went wrong during compilation; get the error 105 | const lastError = this.context.getShaderInfoLog(shader); 106 | 107 | console.error("*** Error compiling shader '" + 108 | shader + 109 | "':" + 110 | lastError + 111 | `\n` + 112 | shaderSource 113 | .split("\n") 114 | .map((l, i) => `${i + 1}: ${l}`) 115 | .join("\n")); 116 | 117 | this.context.deleteShader(shader); 118 | return null; 119 | } 120 | 121 | return shader; 122 | } 123 | 124 | // creates a texture info { width: w, height: h, texture: tex } 125 | // The texture will start with 1x1 pixels and be updated 126 | // when the image has loaded 127 | createTexture() { 128 | let tex = this.context.createTexture() as WebGLTexture; 129 | this.context.bindTexture(this.context.TEXTURE_2D, tex); 130 | // Fill the texture with a 1x1 blue pixel. 131 | this.context.texImage2D(this.context.TEXTURE_2D, 0, this.context.RGBA, 1, 1, 0, this.context.RGBA, this.context.UNSIGNED_BYTE, 132 | new Uint8Array([0, 0, 255, 255])); 133 | 134 | // let's assume all images are not a power of 2 135 | this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_S, this.context.CLAMP_TO_EDGE); 136 | this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_WRAP_T, this.context.CLAMP_TO_EDGE); 137 | this.context.texParameteri(this.context.TEXTURE_2D, this.context.TEXTURE_MIN_FILTER, this.context.LINEAR); 138 | return tex; 139 | } 140 | 141 | // Unlike images, textures do not have a width and height associated 142 | // with them so we'll pass in the width and height of the texture 143 | private drawImage(segment: Segment, element: HTMLVideoElement, properties: KeyFrame) { 144 | if (properties.scaleX as number <= 0 || 145 | properties.scaleY as number <= 0 || 146 | (properties.trimLeft as number) + (properties.trimRight as number) >= 1 || 147 | (properties.trimTop as number) + (properties.trimBottom as number) >= 1) { 148 | return; 149 | } 150 | this.context.bindTexture(this.context.TEXTURE_2D, segment.texture); 151 | this.context.texImage2D(this.context.TEXTURE_2D, 0, this.context.RGBA, this.context.RGBA, this.context.UNSIGNED_BYTE, element); 152 | 153 | // Tell WebGL to use our shader program pair 154 | this.context.useProgram(this.program); 155 | 156 | // Setup the attributes to pull data from our buffers 157 | this.context.bindBuffer(this.context.ARRAY_BUFFER, this.positionBuffer); 158 | this.context.enableVertexAttribArray(this.positionLocation); 159 | this.context.vertexAttribPointer(this.positionLocation, 2, this.context.FLOAT, false, 0, 0); 160 | 161 | // Begin Crop 162 | this.context.bindBuffer(this.context.ARRAY_BUFFER, this.texcoordBuffer); 163 | 164 | // Put texcoords in the buffer 165 | let texcoords = [ 166 | 0 + (properties.trimLeft as number), 0 + (properties.trimTop as number), 167 | 1 - (properties.trimRight as number), 1 - (properties.trimBottom as number), 168 | 1 - (properties.trimRight as number), 0 + (properties.trimTop as number), 169 | 0 + (properties.trimLeft as number), 0 + (properties.trimTop as number), 170 | 0 + (properties.trimLeft as number), 1 - (properties.trimBottom as number), 171 | 1 - (properties.trimRight as number), 1 - (properties.trimBottom as number), 172 | ] 173 | this.context.bufferData(this.context.ARRAY_BUFFER, new Float32Array(texcoords), this.context.STATIC_DRAW); 174 | // End Crop 175 | 176 | this.context.enableVertexAttribArray(this.texcoordLocation); 177 | this.context.vertexAttribPointer(this.texcoordLocation, 2, this.context.FLOAT, false, 0, 0); 178 | 179 | // this matirx will convert from pixels to clip space 180 | let matrix = m4.ortho(0, this.context.canvas.width, this.context.canvas.height, 0, -1, 1); 181 | 182 | let newWidth = element.videoWidth * (properties.scaleX as number); 183 | let newHeight = element.videoHeight * (properties.scaleY as number); 184 | 185 | // this matrix will translate our quad to dstX, dstY 186 | 187 | matrix = m4.translate(matrix, [ 188 | (this.projectWidth / 2) + ((properties.x as number) - (newWidth / 2)) + newWidth * (properties.trimLeft as number), 189 | (this.projectHeight / 2) + ((properties.y as number) - (newHeight / 2)) + newHeight * (properties.trimTop as number), 0, 0]); 190 | 191 | // this matrix will scale our 1 unit quad 192 | // from 1 unit to texWidth, texHeight units 193 | matrix = m4.scale(matrix, [newWidth * (1 - (properties.trimRight as number) - (properties.trimLeft as number)), 194 | newHeight * (1 - (properties.trimTop as number) - (properties.trimBottom as number)), 0, 0]); 195 | 196 | // Set the matrix. 197 | this.context.uniformMatrix4fv(this.matrixLocation, false, matrix); 198 | 199 | // Tell the shader to get the texture from texture unit 0 200 | this.context.uniform1i(this.textureLocation, 0); 201 | 202 | // draw the quad (2 triangles, 6 vertices) 203 | this.context.drawArrays(this.context.TRIANGLES, 0, 6); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /doc/sprint4/sprint4-thenerds.txt: -------------------------------------------------------------------------------- 1 | Team Name: The nerds 2 | 3 | (Convention: N=sprint number, ex: sprintN.md means sprint2.md for sprint 2, and SR(N-1)md means SR1.md for sprint 2, that is Sprint Retrospective for Sprint 1, to be hold during planning meeting for Sprint 2) 4 | 5 | Planning Meetings (SR1.md, sprint2.md) (Max 4 marks) 6 | 7 | -SR1.md (Sprint Retrospective) 8 | [2 marks=Meeting is documented, participants are recorded, everyone has participated, analysis of good practices/bad practices wad made.] 9 | [1 mark=Meeting is documented, participants are not recorded or the majority has not participated, not enough analysis has been done] 10 | [0 marks=No meeting document submitted or the submitted document contains no relevant information] 11 | Mark: 2/2 12 | 13 | -Sprint Planning meeting (sprint2.md) 14 | [2 marks=Meeting and sprint goal is documented, all spikes clearly identified, team capacity recorded, participants are recorded, everyone has participated, 15 | decisions about user stories to be completed this sprint are clear, tasks breakdown is done.] 16 | [1 mark=Meeting and sprint goal is documented, some spikes not clearly identified, one or more members of team's capacity not recorded, participants are not recorded or the majority has not participated, decisions are unclear, 17 | there is no clear evidence which user story will be completed or task breakdown is unclear or incomplete] 18 | [0 marks=No meeting document submitted or the submitted document contains no relevant information to the sprint 1] 19 | Mark: 2/2 20 | 21 | Daily Stand-ups (on Discord 301 server within your team category called #standup) (Max 10 marks, 2 marks per daily standup) 22 | - Team updates each other on what they have done in their #standup Microsoft Teams channel 23 | - 5 standups per sprint required 24 | [2 marks=all teams members have sent their updates in the channel] 25 | [1 mark=atleast 1 group member has not sent their update] 26 | [0 marks=atleast 2 group members or more have not sent their updates] 27 | Mark: 10/10 28 | 29 | User Stories (Tracked in Jira) (Max 10 marks) 30 | -Correctly formulated user stories 31 | [2 marks=all stories in Jira Backlog follow the User Story template] 32 | [1 mark=two or more stories in Jira Backlog do not follow the template] 33 | [0 marks=the majority of stories in Jira Backlog do not follow the template or no stories at all] 34 | Mark: 2/2 35 | 36 | -Broken down into tasks 37 | [2 marks=all Sprint 1 stories estimated, broken into tasks, tasks assigned to all team members] 38 | [1 mark=one or more Sprint 1 stories are not recorded Jira, or not estimated, or nor broken into tasks, or at least one task not assigned, or at least one team member unassigned a task] 39 | [0 marks=the majority of Sprint 1 stories not recorded in Jira or not estimated, or nor broken into tasks, or tasks not assigned, or team members unassigned a task ] 40 | Mark: 2/2 41 | 42 | -Estimated 43 | [2 marks=all stories in Jira Backlog are point estimated and the point value is recorded in Jira] 44 | [1 mark=two or more stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 45 | [0 marks=the majority of stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 46 | Mark: 2/2 47 | 48 | -Prioritized 49 | [2 marks=all stories in Jira Backlog are prioritized in the order which matches the priority of their associated milestones] 50 | [1 mark=two or more stories in Jira Backlog not are prioritized in the order which matches the priority of their associated milestones] 51 | [0 marks=the majority of stories in Jira Backlog appear to be not prioritized in the order which matches the priority of their associated milestones] 52 | Mark: 2/2 53 | 54 | -Clearly Defined Acceptance Criteria 55 | [2 marks=all Sprint 1 stories have clearly defined and testable acceptance criteria] 56 | [1 mark=one or more Sprint 1 stories do not have clearly defined or testable acceptance criteria] 57 | [0 marks=the majority of the Sprint1 stories do not have clearly defined or testable acceptance criteria] 58 | Mark: 2/2 59 | 60 | Tracking on Jira: (Max 24 marks) 61 | - Are all your stories and tasks being worked in this sprint tracked on Active Sprint Board? 62 | [0=no stories on Active Sprint Board] 63 | [1=part of the stories missing] 64 | [2=all stories recorded] 65 | Mark: 2/2 66 | 67 | - Did you start all stories for sprint one? 68 | [0=no story started] 69 | [1=part of the stories not started and explanation why not provided] 70 | [2=all stories started or some stories not started, but logical explanation provided as a story comment] 71 | Mark: 2/2 72 | 73 | - Do the pull request on GitHub match the tasks listed on Jira Active Sprint Board? 74 | [0=no tasks or tasks do not match at all] 75 | [1=two or more tasks are missing] 76 | [2=no tasks are missing] 77 | Mark: 2/2 78 | 79 | - Is the pull request done by the person to whom the task has been assigned to? 80 | [0=no task was completed by anyone] 81 | [1=two or more tasks were not completed as assigned] 82 | [2=all assigned tasks are completed] 83 | Mark: 2/2 84 | 85 | - Is each pull request made from a separate branch for each individual task? 86 | [0=pull request for the majority of tasks not made from own separate branch] 87 | [1=pull request for 4 or more tasks not made from own separate branch] 88 | [2=pull request for 3 or more tasks not made from own separate branch] 89 | [3=pull request for 2 or more tasks not made from own separate branch] 90 | [4=pull request for 1 or more tasks not made from own separate branch] 91 | [5=pull request for each task made from own separate branch] 92 | Mark: 5/5 93 | 94 | - Is the git commit message clear? 95 | [0=majority of commit message do not begin with the ticket number of the task followed by a short description of commit changes] 96 | [1=one or more commit messages do not begin with the ticket number of the task followed by a short description of commit changes] 97 | [2=all commit message begin with the ticket number of the task followed by a short description of commit changes] 98 | Mark: 2/2 99 | 100 | - Tasks Distribution 101 | [0=most of team members have no tasks assigned and/or completed] 102 | [1=at least one team member did not complete any task or did not have any task assigned] 103 | [2=every team member had at least one task assigned and made a pull request] 104 | [3=every team member had at least one task assigned and completed] 105 | Mark: 3/3 106 | 107 | - Branch Naming 108 | [0=most branch names are not named after the associated single ticket number] 109 | [1=at least one branch is not named after the associated single ticket number] 110 | [2=every team branch is named after the associated single ticket number] 111 | Mark: 2/2 112 | 113 | -Burndown chart 114 | [0=document not available or not containing a valid burndown chart] 115 | [1=burndown chart present, matches Jira chart, but no explanations provided, or explanations are not adequate.] 116 | [2=burndown chart present, matched jira chart, and explantions are adequate. 117 | Mark: 2/2 118 | 119 | -Schedule 120 | [0=document not available or not containing a valid diagram] 121 | [1=diagram present, but no critical path or explanations provided, or explanations are not adequate.] 122 | [2=diagram present, critical path computed, and explantions are adequate.] 123 | Mark: 2/2 124 | 125 | Sprint Completion: (Max 20 marks) 126 | - With A Minimum Of 5 Stories 127 | [x = Percentage of story points completed * 20] 128 | Mark: 20/20 129 | 130 | System Design - (PDF or MD file or another suitable format) (Max 10 marks) 131 | - Cover Page and Table of Contents 132 | [2 marks: both are present] 133 | [1 mark: at least one is present] 134 | [0 marks: None present] 135 | Mark: 2/2 136 | 137 | - CRC Cards [or equivalent, if the team is not using CRC) 138 | [4 marks: Class names and Collaborators have matching names and responsibilities are stated clearly] 139 | [3 marks: At least one of the class names does not match the collaborator names or the responsibilities for at least one class are unclear] 140 | [2 marks: Two class names do not match the collaborator names or the responsibilities of two or more classes are not stated or are unclear] 141 | [1 mark: The majority of class names do not match the collaborator names or the responsibilities of the majority of the classes are not stated or are unclear] 142 | [0 marks: No CRC provided or the provided document does not match the CRC model] 143 | Mark: 4/4 144 | 145 | - Software Architecture Diagram 146 | [4 marks: The Architecture Diagram is present, it is formatted using proper graphic symbols, and it follows a known Architecture diagram. 147 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 148 | [3 marks: The Architecture Diagram is present, it is not formatted well, and it follows somewhat a known Architecture diagram. 149 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 150 | [2 marks: The Architecture Diagram is present, it is not formatted well, or it does not follow a known Architecture diagram. 151 | A web link or reference has been provided, but it does not support the used architecture] 152 | [1 mark: The Architecture Diagram is present, it is not formatted well, and it is unclear what Architecture it follows. 153 | There is no web link or other reference lending support to the presented diagram. 154 | [0 marks: No diagram present or the presented document does not look like a software architecture diagram] 155 | Mark: 4/4 156 | 157 | Total: 78/78 158 | Great Job!! Can't wait to see your final presentation!! -------------------------------------------------------------------------------- /doc/sprint2/sprint2-thenerds.txt: -------------------------------------------------------------------------------- 1 | Team Name: 2 | 3 | (Convention: N=sprint number, ex: sprintN.md means sprint2.md for sprint 2, and SR(N-1)md means SR1.md for sprint 2, that is Sprint Retrospective for Sprint 1, to be hold during planning meeting for Sprint 2) 4 | 5 | Planning Meetings (SR1.md, sprint2.md) (Max 4 marks) 6 | 7 | -SR1.md (Sprint Retrospective) 8 | [2 marks=Meeting is documented, participants are recorded, everyone has participated, analysis of good practices/bad practices wad made.] 9 | [1 mark=Meeting is documented, participants are not recorded or the majority has not participated, not enough analysis has been done] 10 | [0 marks=No meeting document submitted or the submitted document contains no relevant information] 11 | 12 | 2/2 13 | 14 | -Sprint Planning meeting (sprint2.md) 15 | [2 marks=Meeting and sprint goal is documented, all spikes clearly identified, team capacity recorded, participants are recorded, everyone has participated, 16 | decisions about user stories to be completed this sprint are clear, tasks breakdown is done.] 17 | [1 mark=Meeting and sprint goal is documented, some spikes not clearly identified, one or more members of team's capacity not recorded, participants are not recorded or the majority has not participated, decisions are unclear, 18 | there is no clear evidence which user story will be completed or task breakdown is unclear or incomplete] 19 | [0 marks=No meeting document submitted or the submitted document contains no relevant information to the sprint 1] 20 | 21 | 2/2 22 | 23 | Daily Stand-ups (on Discord 301 server within your team category called #standup) (Max 10 marks, 2 marks per daily standup) 24 | - Team updates each other on what they have done in their #standup Microsoft Teams channel 25 | - 5 standups per sprint required 26 | [2 marks=all teams members have sent their updates in the channel] 27 | [1 mark=atleast 1 group member has not sent their update] 28 | [0 marks=atleast 2 group members or more have not sent their updates] 29 | 30 | 10/10 31 | 32 | User Stories (Tracked in Jira) (Max 10 marks) 33 | -Correctly formulated user stories 34 | [2 marks=all stories in Jira Backlog follow the User Story template] 35 | [1 mark=two or more stories in Jira Backlog do not follow the template] 36 | [0 marks=the majority of stories in Jira Backlog do not follow the template or no stories at all] 37 | -Broken down into tasks 38 | [2 marks=all Sprint 1 stories estimated, broken into tasks, tasks assigned to all team members] 39 | [1 mark=one or more Sprint 1 stories are not recorded Jira, or not estimated, or nor broken into tasks, or at least one task not assigned, or at least one team member unassigned a task] 40 | [0 marks=the majority of Sprint 1 stories not recorded in Jira or not estimated, or nor broken into tasks, or tasks not assigned, or team members unassigned a task ] 41 | -Estimated 42 | [2 marks=all stories in Jira Backlog are point estimated and the point value is recorded in Jira] 43 | [1 mark=two or more stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 44 | [0 marks=the majority of stories in Jira Backlog are not point estimated or the point value is not recorded in Jira] 45 | -Prioritized 46 | [2 marks=all stories in Jira Backlog are prioritized in the order which matches the priority of their associated milestones] 47 | [1 mark=two or more stories in Jira Backlog not are prioritized in the order which matches the priority of their associated milestones] 48 | [0 marks=the majority of stories in Jira Backlog appear to be not prioritized in the order which matches the priority of their associated milestones] 49 | -Clearly Defined Acceptance Criteria 50 | [2 marks=all Sprint 1 stories have clearly defined and testable acceptance criteria] 51 | [1 mark=one or more Sprint 1 stories do not have clearly defined or testable acceptance criteria] 52 | [0 marks=the majority of the Sprint1 stories do not have clearly defined or testable acceptance criteria] 53 | 54 | 10/10 55 | 56 | 57 | Tracking on Jira: (Max 24 marks) 58 | - Are all your stories and tasks being worked in this sprint tracked on Active Sprint Board? 59 | [0=no stories on Active Sprint Board] 60 | [1=part of the stories missing] 61 | [2=all stories recorded] 62 | 63 | 2/2 64 | 65 | - Did you start all stories for sprint one? 66 | [0=no story started] 67 | [1=part of the stories not started and explanation why not provided] 68 | [2=all stories started or some stories not started, but logical explanation provided as a story comment] 69 | 70 | 2/2 71 | 72 | - Do the pull request on GitHub match the tasks listed on Jira Active Sprint Board? 73 | [0=no tasks or tasks do not match at all] 74 | [1=two or more tasks are missing] 75 | [2=no tasks are missing] 76 | 77 | 2/2 78 | 79 | - Is the pull request done by the person to whom the task has been assigned to? 80 | [0=no task was completed by anyone] 81 | [1=two or more tasks were not completed as assigned] 82 | [2=all assigned tasks are completed] 83 | 84 | 2/2 85 | 86 | - Is each pull request made from a separate branch for each individual task? 87 | [0=pull request for the majority of tasks not made from own separate branch] 88 | [1=pull request for 4 or more tasks not made from own separate branch] 89 | [2=pull request for 3 or more tasks not made from own separate branch] 90 | [3=pull request for 2 or more tasks not made from own separate branch] 91 | [4=pull request for 1 or more tasks not made from own separate branch] 92 | [5=pull request for each task made from own separate branch] 93 | 94 | 5/5 95 | 96 | - Is the git commit message clear? 97 | [0=majority of commit message do not begin with the ticket number of the task followed by a short description of commit changes] 98 | [1=one or more commit messages do not begin with the ticket number of the task followed by a short description of commit changes] 99 | [2=all commit message begin with the ticket number of the task followed by a short description of commit changes] 100 | 101 | 2/2 102 | 103 | - Tasks Distribution 104 | [0=most of team members have no tasks assigned and/or completed] 105 | [1=at least one team member did not complete any task or did not have any task assigned] 106 | [2=every team member had at least one task assigned and made a pull request] 107 | [3=every team member had at least one task assigned and completed] 108 | 109 | 3/3 110 | 111 | - Branch Naming 112 | [0=most branch names are not named after the associated single ticket number] 113 | [1=at least one branch is not named after the associated single ticket number] 114 | [2=every team branch is named after the associated single ticket number] 115 | 116 | 2/2 117 | 118 | -Burndown chart 119 | [0=document not available or not containing a valid burndown chart] 120 | [1=burndown chart present, matches Jira chart, but no explanations provided, or explanations are not adequate.] 121 | [2=burndown chart present, matched jira chart, and explantions are adequate. 122 | 123 | 2/2 124 | 125 | TA FEEDBACK: So i noticed that the burndown flatlined quite a bit, just want you all 126 | to reflect on why? Because Burndowns are typically supposed follow as close as you can 127 | to the grey line, in order to keep you all on track. 128 | 129 | -Schedule 130 | [0=document not available or not containing a valid diagram] 131 | [1=diagram present, but no critical path or explanations provided, or explanations are not adequate.] 132 | [2=diagram present, critical path computed, and explantions are adequate.] 133 | 134 | 2/2 135 | 136 | Sprint Completion: (Max 20 marks) 137 | - With A Minimum Of 5 Stories 138 | [x = Percentage of story points completed * 20] 139 | 140 | 20/20 141 | 142 | System Design - (PDF or MD file or another suitable format) (Max 10 marks) 143 | - Cover Page and Table of Contents 144 | [2 marks: both are present] 145 | [1 mark: at least one is present] 146 | [0 marks: None present] 147 | 148 | 2/2 149 | 150 | - CRC Cards [or equivalent, if the team is not using CRC) 151 | [4 marks: Class names and Collaborators have matching names and responsibilities are stated clearly] 152 | [3 marks: At least one of the class names does not match the collaborator names or the responsibilities for at least one class are unclear] 153 | [2 marks: Two class names do not match the collaborator names or the responsibilities of two or more classes are not stated or are unclear] 154 | [1 mark: The majority of class names do not match the collaborator names or the responsibilities of the majority of the classes are not stated or are unclear] 155 | [0 marks: No CRC provided or the provided document does not match the CRC model] 156 | 157 | 4/4 158 | 159 | - Software Architecture Diagram 160 | [4 marks: The Architecture Diagram is present, it is formatted using proper graphic symbols, and it follows a known Architecture diagram. 161 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 162 | [3 marks: The Architecture Diagram is present, it is not formatted well, and it follows somewhat a known Architecture diagram. 163 | If the system architecture does not follow the MVC (studied in class), a web link or reference has been provided to support the used architecture] 164 | [2 marks: The Architecture Diagram is present, it is not formatted well, or it does not follow a known Architecture diagram. 165 | A web link or reference has been provided, but it does not support the used architecture] 166 | [1 mark: The Architecture Diagram is present, it is not formatted well, and it is unclear what Architecture it follows. 167 | There is no web link or other reference lending support to the presented diagram. 168 | [0 marks: No diagram present or the presented document does not look like a software architecture diagram] 169 | 170 | 4/4 171 | 172 | 10/10 173 | 174 | FINAL MARK: 78/78, nice job team! -------------------------------------------------------------------------------- /fire/src/model/mediaManager.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from "react"; 2 | import { calculateProperties } from "../utils/utils"; 3 | import PlaybackController from "./playbackController"; 4 | import { Media, Project, Segment, SegmentID } from "./types"; 5 | import { WebGLRenderer } from "./webgl"; 6 | 7 | export default function MediaManager(props: { 8 | setProjects: (projects: Project[]) => void; 9 | projects: Project[]; 10 | projectUser: string; 11 | setProjectUser: (user: string) => void; 12 | projectHeight: number; 13 | setProjectHeight: (height: number) => void; 14 | projectWidth: number; 15 | setProjectWidth: (width: number) => void; 16 | projectFramerate: number; 17 | setProjectFramerate: (framerate: number) => void; 18 | projectName: string; 19 | setProjectName: (name: string) => void; 20 | projectId: string; 21 | setProjectId: (id: string) => void; 22 | projectDuration: number; 23 | setProjectDuration: (duration: number) => void; 24 | }) { 25 | const [mediaList, setMediaList] = useState([]); 26 | const [trackList, setTrackList] = useState([[]]); 27 | const [selectedSegment, setSelectedSegment] = useState(null); 28 | const [canvasRef, setCanvasRef] = useState(document.createElement("canvas")); 29 | const [renderer, setRenderer] = useState(new WebGLRenderer(canvasRef, props.projectWidth, props.projectHeight)); 30 | 31 | useEffect(() => { 32 | canvasRef.width = props.projectWidth; 33 | canvasRef.height = props.projectHeight; 34 | }, [canvasRef, props.projectHeight, props.projectWidth]); 35 | 36 | useEffect(() => { 37 | let duration = 0; 38 | for (const track of trackList) { 39 | if (track.length === 0) continue; 40 | duration = Math.max(duration, track[track.length - 1].start + track[track.length - 1].duration); 41 | } 42 | 43 | props.setProjectDuration(duration); 44 | }, [trackList]); 45 | 46 | const thumbnailCanvas = document.createElement("canvas"); 47 | const thumbnailCanvasContext = thumbnailCanvas.getContext("2d") as CanvasRenderingContext2D; 48 | 49 | const generateThumbnail = async (file: File) => { 50 | let elm = document.createElement("video") as HTMLVideoElement; 51 | elm.preload = "auto"; 52 | 53 | await new Promise((resolve, reject) => { 54 | elm.onloadeddata = () => resolve(); 55 | elm.src = URL.createObjectURL(file); 56 | elm.currentTime = 0.0001; 57 | }); 58 | 59 | // Generate Thumbnail 60 | thumbnailCanvas.width = elm.videoWidth; 61 | thumbnailCanvas.height = elm.videoHeight; 62 | thumbnailCanvasContext.drawImage( 63 | elm, 64 | 0, 65 | 0, 66 | elm.videoWidth, 67 | elm.videoHeight 68 | ); 69 | 70 | let media: Media = { 71 | sources: [{ track: 0, element: elm, inUse: false }], 72 | file: file, 73 | thumbnail: thumbnailCanvas.toDataURL(), 74 | }; 75 | 76 | return media; 77 | }; 78 | 79 | const addVideo = async (files: File[]) => { 80 | let uniqueFiles: File[] = []; 81 | let found = false; 82 | for (let file of files) { 83 | for (let i = 0; i < mediaList.length; i++) { 84 | if (mediaList[i].file.name === file.name) { 85 | found = true; 86 | break; 87 | } 88 | } 89 | if (found) continue; 90 | uniqueFiles.push(file); 91 | } 92 | 93 | let filesList: Media[] = []; 94 | 95 | for (let file of uniqueFiles) { 96 | filesList.push(await generateThumbnail(file)); 97 | } 98 | 99 | setMediaList([...mediaList, ...filesList]); 100 | 101 | console.log("Sucessfully Loaded Segment Thumbnail!"); 102 | return; 103 | } 104 | 105 | const dragAndDrop = (media: Media) => { 106 | if (renderer == null) return; 107 | let segment: Segment = { 108 | media: media, 109 | start: 0, 110 | duration: media.sources[0].element.duration * 1000, 111 | mediaStart: 0, 112 | texture: renderer.createTexture(), 113 | keyframes: [ 114 | { 115 | start: 0, 116 | x: 0, 117 | y: 0, 118 | trimRight: 0, 119 | trimLeft: 0, 120 | trimTop: 0, 121 | trimBottom: 0, 122 | scaleX: 1.0, 123 | scaleY: 1.0, 124 | }, 125 | ] 126 | }; 127 | 128 | let newElement = media.sources[0].element.cloneNode() as HTMLVideoElement; 129 | newElement.pause(); 130 | 131 | if (trackList[trackList.length - 1].length === 0) { 132 | if (!media.sources.find(source => source.track === trackList.length - 1)) 133 | media.sources.push({ track: trackList.length - 1, element: newElement, inUse: false }); 134 | setTrackList([...trackList.slice(0, trackList.length - 1), [segment], []]); 135 | } else { 136 | media.sources.push({ track: trackList.length, element: newElement, inUse: false }); 137 | setTrackList([...trackList, [segment], []]); 138 | } 139 | } 140 | 141 | const deleteVideo = (media: Media) => { 142 | for (const source of media.sources) { 143 | source.element.pause(); 144 | } 145 | 146 | if (selectedSegment && trackList[selectedSegment.track][selectedSegment.index].media === media) setSelectedSegment(null); 147 | setMediaList(mediaList.filter(item => item !== media)); 148 | 149 | let newTrackList = trackList.map(track => track.filter(segment => segment.media !== media)); 150 | 151 | // Clean Tracklist 152 | while (newTrackList.length > 0 && newTrackList[newTrackList.length - 1].length === 0) newTrackList.pop(); 153 | newTrackList.push([]); 154 | 155 | setTrackList(newTrackList); 156 | } 157 | 158 | const deleteSelectedSegment = () => { 159 | if (selectedSegment === null) return; 160 | 161 | for (const source of trackList[selectedSegment.track][selectedSegment.index].media.sources) { 162 | source.element.pause(); 163 | } 164 | 165 | let newTrackList = [ 166 | ...trackList.slice(0, selectedSegment.track), 167 | [...trackList[selectedSegment.track].slice(0, selectedSegment.index), ...trackList[selectedSegment.track].slice(selectedSegment.index + 1)], 168 | ...trackList.slice(selectedSegment.track + 1) 169 | ]; 170 | 171 | // Clean Tracklist 172 | while (newTrackList.length > 0 && newTrackList[newTrackList.length - 1].length === 0) newTrackList.pop(); 173 | newTrackList.push([]); 174 | 175 | setTrackList(newTrackList); 176 | setSelectedSegment(null); 177 | } 178 | 179 | const split = (timestamp: number) => { 180 | 181 | if (selectedSegment === null) return; 182 | 183 | const segment = trackList[selectedSegment.track][selectedSegment.index]; 184 | 185 | if (segment.start > timestamp || segment.start + segment.duration < timestamp) return; 186 | 187 | // Find index of current keyframe at timestamp 188 | // There is always at least 1 keyframe in a segment 189 | 190 | let segmentTimeCut = timestamp - segment.start; 191 | let lenKeyframes = segment.keyframes.length; 192 | let keyFrameIndex = 0; 193 | for (let i = 1; i < lenKeyframes; i++) { 194 | let checkKeyframe = segment.keyframes[i]; 195 | if (checkKeyframe.start > segmentTimeCut) { 196 | break; 197 | } 198 | keyFrameIndex = i; 199 | } 200 | 201 | let interpKeyFrame = calculateProperties(segment, timestamp); 202 | 203 | // Remove remaining keyframes from split segment 204 | let leftSegmentKeyFrames = segment.keyframes.slice(0, keyFrameIndex + 1); 205 | // Move remaining keyframes to new split segment 206 | let rightSegmentKeyFrames = segment.keyframes.slice(keyFrameIndex + 1, lenKeyframes); 207 | 208 | // Edit new keyframes to new offset 209 | for (let i = 0; i < rightSegmentKeyFrames.length; i++) { 210 | rightSegmentKeyFrames[i].start -= segmentTimeCut; 211 | } 212 | 213 | // Add interpolated keyframe at the end of the selected split segement 214 | let newInterpKeyFrame = { 215 | ...interpKeyFrame, 216 | start: segmentTimeCut - 1 / props.projectFramerate 217 | }; 218 | leftSegmentKeyFrames.push(newInterpKeyFrame); 219 | 220 | setTrackList([ 221 | ...trackList.slice(0, selectedSegment.track), 222 | [ 223 | ...trackList[selectedSegment.track].slice(0, selectedSegment.index), 224 | { 225 | ...trackList[selectedSegment.track][selectedSegment.index], duration: timestamp - segment.start, 226 | keyframes: leftSegmentKeyFrames 227 | }, 228 | { 229 | media: segment.media, 230 | start: timestamp, 231 | duration: segment.start + segment.duration - timestamp, 232 | mediaStart: timestamp - segment.start + segment.mediaStart, 233 | texture: segment.texture, 234 | keyframes: [interpKeyFrame].concat(rightSegmentKeyFrames) 235 | }, 236 | ...trackList[selectedSegment.track].slice(selectedSegment.index + 1) 237 | ], 238 | ...trackList.slice(selectedSegment.track + 1) 239 | ]); 240 | } 241 | 242 | const updateSegment = (id: SegmentID, newSegment: Segment) => { 243 | setTrackList([ 244 | ...trackList.slice(0, id.track), 245 | [...trackList[id.track].slice(0, id.index), newSegment, ...trackList[id.track].slice(id.index + 1)], 246 | ...trackList.slice(id.track + 1) 247 | ]); 248 | } 249 | 250 | return ( 251 | 268 | ); 269 | } 270 | -------------------------------------------------------------------------------- /fire/src/components/timeline/timeline.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./timeline.module.css"; 2 | import { Segment, SegmentID } from "../../model/types"; 3 | import { useState, MouseEvent, useRef, useEffect } from "react"; 4 | import { lerp } from "../../utils/utils"; 5 | import { Droppable } from 'react-beautiful-dnd'; 6 | 7 | export default function Timeline({ 8 | trackList, 9 | projectDuration, 10 | selectedSegment, 11 | setSelectedSegment, 12 | currentTime, 13 | setCurrentTime, 14 | updateSegment, 15 | scaleFactor, 16 | setTrackList, 17 | }: { 18 | trackList: Segment[][], 19 | projectDuration: number, 20 | selectedSegment: SegmentID | null, 21 | setSelectedSegment: (selected: SegmentID | null) => void, 22 | currentTime: number, 23 | setCurrentTime: (timestamp: number) => void, 24 | updateSegment: (id: SegmentID, segment: Segment) => void, 25 | scaleFactor: number, 26 | setTrackList: (tracks: Segment[][]) => void 27 | }) { 28 | enum DragMode { 29 | NONE, 30 | MOVE, 31 | TRIM_LEFT, 32 | TRIM_RIGHT 33 | } 34 | 35 | const COLORS = [[255, 0, 0], [82, 0, 255], [0, 255, 15], [234, 0, 255], [25, 0, 255], [255, 231, 0]]; 36 | const RESIZE_OFFSET = 10; 37 | const COLOR_MULTIPLER = 0.5; 38 | const timeout = useRef(0); 39 | const containerRef = useRef(null); 40 | const [trackDivs, setTrackDivs] = useState([]); 41 | const segmentStartOffsetRef = useRef(0); 42 | const clickStartTimeRef = useRef(-1); 43 | const dragMode = useRef(DragMode.NONE); 44 | 45 | const formatTime = (time: number) => { 46 | let s = parseFloat((time / 1000).toFixed(2)); 47 | 48 | let currentMinute = Math.floor(s / 60); 49 | let currentSecond = Math.floor(s / 1) - currentMinute * 60; 50 | let currentMillisecond = (s - Math.floor(s / 1))*100; 51 | currentSecond = currentSecond + Math.floor(currentMillisecond/30); 52 | currentMillisecond = currentMillisecond - 30*Math.floor(currentMillisecond/30); 53 | let minute = String(currentMinute); 54 | while (minute.length < (2 || 2)) { minute = "0" + minute; } 55 | let second = String(currentSecond); 56 | while (second.length < (2 || 2)) { second = "0" + second; } 57 | let millisecond = currentMillisecond.toFixed(0); 58 | while (millisecond.length < (2 || 2)) { millisecond = "0" + millisecond; } 59 | 60 | return minute + ":" + second+ ":" + millisecond; 61 | } 62 | 63 | const ruler = () => { 64 | if (!containerRef.current) return; 65 | let rows: any[] = []; 66 | 67 | let length = Math.max(containerRef.current.clientWidth / scaleFactor, projectDuration); 68 | let divisions = Math.floor(length * scaleFactor / 100); 69 | 70 | for (let i = 0; i < divisions; i++) { 71 | let time = length / divisions * i; 72 | rows.push( 73 |
    { 76 | event.stopPropagation(); 77 | setCurrentTime(lerp(time, length / divisions * (i + 1), event.nativeEvent.offsetX / ((length / divisions) * scaleFactor))); 78 | }} > 79 |

    {formatTime(time)}

    80 |
    81 |
    82 |
    83 |
    84 |
    85 | {i === 0 ?
    : ''} 86 |
    87 | ) 88 | } 89 | 90 | return
    {rows}
    ; 91 | } 92 | 93 | const calculateHoverState = (event: MouseEvent) => { 94 | if (containerRef.current === null || clickStartTimeRef.current !== -1) return; 95 | event.stopPropagation(); 96 | event.preventDefault(); 97 | 98 | if (event.nativeEvent.offsetX < RESIZE_OFFSET) { 99 | setDragMode(DragMode.TRIM_LEFT); 100 | } else if (event.nativeEvent.offsetX > (event.nativeEvent.target as HTMLDivElement).clientWidth - RESIZE_OFFSET) { 101 | setDragMode(DragMode.TRIM_RIGHT); 102 | } else { 103 | setDragMode(DragMode.MOVE); 104 | } 105 | }; 106 | 107 | const genTrack = (segments: Segment[], trackInd: number) => { 108 | let segmentDivs = []; 109 | 110 | for (let i = 0; i < segments.length; i++) { 111 | const segment = segments[i]; 112 | const color = COLORS[i % COLORS.length]; 113 | const isSelected = selectedSegment !== null && selectedSegment.track === trackInd && selectedSegment.index === i; 114 | 115 | let space = segment.start - (i === 0 ? 0 : (segments[i - 1].start + segments[i - 1].duration)); 116 | segmentDivs.push(
    ); 117 | 118 | segmentDivs.push( 119 |
    { 124 | event.stopPropagation(); 125 | }} 126 | > 127 |
    { 140 | if (containerRef.current === null || event.button !== 0) return; 141 | event.stopPropagation(); 142 | setSelectedSegment({ track: trackInd, index: i }); 143 | 144 | clickStartTimeRef.current = (event.nativeEvent.clientX - containerRef.current.getBoundingClientRect().left + containerRef.current.scrollLeft) / scaleFactor; 145 | segmentStartOffsetRef.current = clickStartTimeRef.current - trackList[trackInd][i].start; 146 | 147 | calculateHoverState(event); 148 | }} 149 | 150 | onMouseMove={calculateHoverState} 151 | 152 | onMouseLeave={() => { if (clickStartTimeRef.current === -1) setDragMode(DragMode.NONE); }} 153 | > 154 |
    155 | {isSelected ?
    156 | {segment.keyframes.map((keyframe) => { 157 | if (segment.keyframes.length > 1) { 158 | return ( 159 | 173 | ); 174 | } 175 | else { 176 | return (""); 177 | } 178 | })} 179 |
    : ""} 180 |
    181 | ); 182 | } 183 | 184 | return segmentDivs; 185 | } 186 | 187 | useEffect(() => { 188 | setTrackDivs(trackList.map((track, ind) =>
    {genTrack(track, ind)}
    )); 189 | }, [trackList, selectedSegment, scaleFactor]); 190 | 191 | useEffect(() => { 192 | let listener = (event: globalThis.MouseEvent) => { 193 | if (event.button === 0 && clickStartTimeRef.current !== -1) { 194 | event.stopPropagation(); 195 | event.preventDefault(); 196 | setDragMode(DragMode.NONE); 197 | clickStartTimeRef.current = -1; 198 | } 199 | } 200 | 201 | document.addEventListener("mouseup", listener); 202 | return () => { document.removeEventListener("mouseup", listener); }; 203 | }, []) 204 | 205 | const setDragMode = (mode: DragMode) => { 206 | dragMode.current = mode; 207 | 208 | if (mode === DragMode.MOVE) { 209 | document.body.style.cursor = "move"; 210 | } else if (mode === DragMode.TRIM_LEFT) { 211 | document.body.style.cursor = "ew-resize"; 212 | } else if (mode === DragMode.TRIM_RIGHT) { 213 | document.body.style.cursor = "ew-resize"; 214 | } else { 215 | document.body.style.cursor = ""; 216 | } 217 | } 218 | 219 | const clamp = (value: number, min: number, max: number) => { 220 | return Math.max(min, Math.min(max, value)); 221 | } 222 | 223 | const handleMove = (event: MouseEvent) => { 224 | timeout.current = 0; 225 | if (selectedSegment === null || containerRef.current === null || clickStartTimeRef.current === -1) return; 226 | 227 | const segment = trackList[selectedSegment.track][selectedSegment.index]; 228 | 229 | let end = (event.nativeEvent.clientX - containerRef.current.getBoundingClientRect().left + containerRef.current.scrollLeft) / scaleFactor; 230 | let change = end - clickStartTimeRef.current; 231 | clickStartTimeRef.current = end; 232 | 233 | let y = event.nativeEvent.clientY - containerRef.current.getBoundingClientRect().top + containerRef.current.scrollTop; 234 | let track = Math.max(Math.floor((y - 40) / 81), 0); 235 | 236 | if (dragMode.current === DragMode.MOVE) { 237 | // Create a new track 238 | if (track >= trackList.length - 1) { 239 | trackList.push([]); 240 | track = trackList.length - 2; 241 | } 242 | 243 | let requestedTime = Math.max(end - segmentStartOffsetRef.current, 0); 244 | let foundTime = Infinity; 245 | let startTime = 0; 246 | let endTime = 0; 247 | 248 | // Search for closest opening to the requested time 249 | for (let i = 0; i < trackList[track].length + 1; i++) { 250 | if (selectedSegment.track === track && selectedSegment.index === i) continue; 251 | 252 | endTime = i < trackList[track].length ? trackList[track][i].start : Infinity; 253 | 254 | if (segment.duration <= endTime - startTime) { 255 | if (requestedTime >= startTime && requestedTime + segment.duration < endTime) { 256 | foundTime = requestedTime; 257 | break; 258 | } 259 | 260 | let newTime = requestedTime < startTime ? startTime : endTime - segment.duration; 261 | 262 | if (Math.abs(requestedTime - newTime) < Math.abs(requestedTime - foundTime)) { 263 | foundTime = newTime; 264 | } 265 | } 266 | 267 | if (i < trackList[track].length) { 268 | startTime = trackList[track][i].start + trackList[track][i].duration; 269 | } 270 | } 271 | 272 | // Setup new element for track 273 | if (track !== selectedSegment.track) { 274 | if (!segment.media.sources.find(source => source.track === track)) { 275 | let newElement = segment.media.sources[0].element.cloneNode() as HTMLVideoElement; 276 | newElement.pause(); 277 | segment.media.sources.push({ track: track, element: newElement, inUse: false }); 278 | } 279 | } 280 | 281 | // Insert new Segment and remove old 282 | let newSegment = { ...segment, start: foundTime }; 283 | let insertedInd = -1; 284 | let newTrackList = []; 285 | for (let i = 0; i < trackList.length; i++) { 286 | let x = []; 287 | for (let j = 0; j < trackList[i].length; j++) { 288 | if (i === selectedSegment.track && j === selectedSegment.index) continue; 289 | 290 | if (i === track && insertedInd === -1 && trackList[i][j].start > foundTime) { 291 | x.push(newSegment); 292 | insertedInd = x.length - 1; 293 | } 294 | x.push(trackList[i][j]); 295 | } 296 | if (i === track && insertedInd === -1) { 297 | x.push(newSegment); 298 | insertedInd = x.length - 1; 299 | } 300 | newTrackList.push(x); 301 | } 302 | 303 | while (newTrackList.length > 0 && newTrackList[newTrackList.length - 1].length === 0) newTrackList.pop(); 304 | newTrackList.push([]); 305 | 306 | setTrackList(newTrackList); 307 | setSelectedSegment({ track: track, index: insertedInd }); 308 | } else if (dragMode.current === DragMode.TRIM_LEFT) { 309 | change = clamp(change, -segment.mediaStart, Math.min(segment.media.sources[0].element.duration * 1000 - segment.mediaStart, segment.duration)); 310 | 311 | if (selectedSegment.index > 0) { 312 | change = Math.max(change, -segment.start + trackList[selectedSegment.track][selectedSegment.index - 1].start + 313 | trackList[selectedSegment.track][selectedSegment.index - 1].duration); 314 | } 315 | 316 | updateSegment(selectedSegment, { ...segment, start: segment.start + change, mediaStart: segment.mediaStart + change, duration: segment.duration - change }); 317 | } else if (dragMode.current === DragMode.TRIM_RIGHT) { 318 | change = clamp(change, -segment.duration, segment.media.sources[0].element.duration * 1000 - segment.mediaStart - segment.duration); 319 | 320 | if (selectedSegment.index < trackList[selectedSegment.track].length - 1) { 321 | change = Math.min(change, trackList[selectedSegment.track][selectedSegment.index + 1].start - segment.start - segment.duration); 322 | } 323 | 324 | updateSegment(selectedSegment, { ...segment, duration: segment.duration + change }); 325 | } 326 | } 327 | 328 | const onMouseMove = (event: MouseEvent) => { 329 | event.stopPropagation(); 330 | event.preventDefault(); 331 | 332 | if (clickStartTimeRef.current !== -1 && timeout.current === 0) { 333 | timeout.current = setTimeout(() => { handleMove(event); }, 16) as unknown as number; 334 | } 335 | } 336 | 337 | return ( 338 |
    setSelectedSegment(null)} 340 | onMouseMove={onMouseMove} 341 | ref={containerRef} 342 | > 343 | 344 | { 345 | (provided) => ( 346 |
    347 |
    348 | {ruler()} 349 |
    352 |
    353 |
    354 |
    355 | {trackDivs} 356 |
    357 | {provided.placeholder} 358 |
    359 | ) 360 | } 361 |
    362 |
    363 | ); 364 | } 365 | --------------------------------------------------------------------------------