├── .browserslistrc ├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── angular.json ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── features │ │ ├── chalk-boom-overlay │ │ │ ├── chalk-boom-overlay-routing.module.ts │ │ │ ├── chalk-boom-overlay.component.html │ │ │ ├── chalk-boom-overlay.component.scss │ │ │ ├── chalk-boom-overlay.component.spec.ts │ │ │ ├── chalk-boom-overlay.component.ts │ │ │ ├── chalk-boom-overlay.module.ts │ │ │ ├── components │ │ │ │ ├── boom │ │ │ │ │ ├── boom.component.html │ │ │ │ │ ├── boom.component.scss │ │ │ │ │ ├── boom.component.spec.ts │ │ │ │ │ └── boom.component.ts │ │ │ │ ├── rocket │ │ │ │ │ ├── rocket.component.html │ │ │ │ │ ├── rocket.component.scss │ │ │ │ │ ├── rocket.component.spec.ts │ │ │ │ │ └── rocket.component.ts │ │ │ │ ├── score │ │ │ │ │ ├── score.component.html │ │ │ │ │ ├── score.component.scss │ │ │ │ │ ├── score.component.spec.ts │ │ │ │ │ └── score.component.ts │ │ │ │ └── timer │ │ │ │ │ ├── timer.component.html │ │ │ │ │ ├── timer.component.scss │ │ │ │ │ ├── timer.component.spec.ts │ │ │ │ │ └── timer.component.ts │ │ │ └── services │ │ │ │ ├── chalk-boom.service.spec.ts │ │ │ │ └── chalk-boom.service.ts │ │ ├── chalkboard-overlay │ │ │ ├── chalkboard-overlay-routing.module.ts │ │ │ ├── chalkboard-overlay.component.html │ │ │ ├── chalkboard-overlay.component.scss │ │ │ ├── chalkboard-overlay.component.spec.ts │ │ │ ├── chalkboard-overlay.component.ts │ │ │ ├── chalkboard-overlay.module.ts │ │ │ ├── components │ │ │ │ ├── behind-you │ │ │ │ │ ├── behind-you.component.html │ │ │ │ │ ├── behind-you.component.scss │ │ │ │ │ ├── behind-you.component.spec.ts │ │ │ │ │ └── behind-you.component.ts │ │ │ │ ├── bring-finite-water │ │ │ │ │ ├── bring-finite-water.component.html │ │ │ │ │ ├── bring-finite-water.component.scss │ │ │ │ │ ├── bring-finite-water.component.spec.ts │ │ │ │ │ └── bring-finite-water.component.ts │ │ │ │ ├── capt-jack-review │ │ │ │ │ ├── capt-jack-review.component.html │ │ │ │ │ ├── capt-jack-review.component.scss │ │ │ │ │ ├── capt-jack-review.component.spec.ts │ │ │ │ │ └── capt-jack-review.component.ts │ │ │ │ ├── chalkboard-cheer │ │ │ │ │ ├── chalkboard-cheer.component.html │ │ │ │ │ ├── chalkboard-cheer.component.scss │ │ │ │ │ ├── chalkboard-cheer.component.spec.ts │ │ │ │ │ └── chalkboard-cheer.component.ts │ │ │ │ ├── chalkboard-gift-sub │ │ │ │ │ ├── chalkboard-gift-sub.component.html │ │ │ │ │ ├── chalkboard-gift-sub.component.scss │ │ │ │ │ ├── chalkboard-gift-sub.component.spec.ts │ │ │ │ │ └── chalkboard-gift-sub.component.ts │ │ │ │ ├── chalkboard-point-test │ │ │ │ │ ├── chalkboard-point-test.component.html │ │ │ │ │ ├── chalkboard-point-test.component.scss │ │ │ │ │ ├── chalkboard-point-test.component.spec.ts │ │ │ │ │ └── chalkboard-point-test.component.ts │ │ │ │ ├── chalkboard-raid │ │ │ │ │ ├── chalkboard-raid.component.html │ │ │ │ │ ├── chalkboard-raid.component.scss │ │ │ │ │ ├── chalkboard-raid.component.spec.ts │ │ │ │ │ └── chalkboard-raid.component.ts │ │ │ │ ├── chalkboard-sub-no-message │ │ │ │ │ ├── chalkboard-sub-no-message.component.html │ │ │ │ │ ├── chalkboard-sub-no-message.component.scss │ │ │ │ │ ├── chalkboard-sub-no-message.component.spec.ts │ │ │ │ │ └── chalkboard-sub-no-message.component.ts │ │ │ │ ├── chalkboard-subscribe │ │ │ │ │ ├── chalkboard-subscribe.component.html │ │ │ │ │ ├── chalkboard-subscribe.component.scss │ │ │ │ │ ├── chalkboard-subscribe.component.spec.ts │ │ │ │ │ └── chalkboard-subscribe.component.ts │ │ │ │ └── message-matrix │ │ │ │ │ ├── message-matrix.component.html │ │ │ │ │ ├── message-matrix.component.scss │ │ │ │ │ ├── message-matrix.component.spec.ts │ │ │ │ │ └── message-matrix.component.ts │ │ │ └── services │ │ │ │ ├── cheer-component.service.spec.ts │ │ │ │ └── cheer-component.service.ts │ │ ├── circles-binning │ │ │ ├── circles-binning-routing.module.ts │ │ │ ├── circles-binning.component.html │ │ │ ├── circles-binning.component.scss │ │ │ ├── circles-binning.component.spec.ts │ │ │ ├── circles-binning.component.ts │ │ │ └── circles-binning.module.ts │ │ ├── circles │ │ │ ├── circles-routing.module.ts │ │ │ ├── circles.component.html │ │ │ ├── circles.component.scss │ │ │ ├── circles.component.spec.ts │ │ │ ├── circles.component.ts │ │ │ └── circles.module.ts │ │ ├── general-overlay │ │ │ ├── components │ │ │ │ └── follow │ │ │ │ │ ├── follow.component.html │ │ │ │ │ ├── follow.component.scss │ │ │ │ │ ├── follow.component.spec.ts │ │ │ │ │ └── follow.component.ts │ │ │ ├── general-overlay-routing.module.ts │ │ │ ├── general-overlay.component.html │ │ │ ├── general-overlay.component.scss │ │ │ ├── general-overlay.component.spec.ts │ │ │ ├── general-overlay.component.ts │ │ │ └── general-overlay.module.ts │ │ └── toggle-channel-points │ │ │ ├── toggle-channel-points-routing.module.ts │ │ │ ├── toggle-channel-points.component.html │ │ │ ├── toggle-channel-points.component.scss │ │ │ ├── toggle-channel-points.component.spec.ts │ │ │ ├── toggle-channel-points.component.ts │ │ │ └── toggle-channel-points.module.ts │ ├── models │ │ ├── boom-circle.ts │ │ ├── bounding-box.ts │ │ ├── circle.ts │ │ ├── emote-message.ts │ │ └── point.ts │ └── services │ │ ├── obs-websockets.service.spec.ts │ │ ├── obs-websockets.service.ts │ │ ├── twitch-events.service.spec.ts │ │ └── twitch-events.service.ts ├── assets │ ├── .gitkeep │ ├── brush-bg-1.png │ └── brush-mask-1.png ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── tslint.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.155.1/containers/typescript-node/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Node.js version: 14, 12, 10 4 | ARG VARIANT="14-buster" 5 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT} 6 | 7 | # [Optional] Uncomment this section to install additional OS packages. 8 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | # && apt-get -y install --no-install-recommends 10 | 11 | # [Optional] Uncomment if you want to install an additional version of node using nvm 12 | # ARG EXTRA_NODE_VERSION=10 13 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 14 | 15 | # [Optional] Uncomment if you want to install more global node packages 16 | RUN npm install -g @angular/cli 17 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.155.1/containers/typescript-node 3 | { 4 | "name": "Node.js & TypeScript", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "14" 10 | } 11 | }, 12 | 13 | // Set *default* container specific settings.json values on container create. 14 | "settings": { 15 | "terminal.integrated.shell.linux": "/bin/bash" 16 | }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | "extensions": [ 20 | "dbaeumer.vscode-eslint" 21 | ], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "yarn install", 28 | 29 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 30 | // "remoteUser": "node" 31 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | 48 | /.vscode 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 FiniteSingularity 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Finitebot 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.1.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "finitebot": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/finitebot", 24 | "index": "src/index.html", 25 | "main": "src/main.ts", 26 | "polyfills": "src/polyfills.ts", 27 | "tsConfig": "tsconfig.app.json", 28 | "aot": true, 29 | "assets": [ 30 | "src/favicon.ico", 31 | "src/assets" 32 | ], 33 | "styles": [ 34 | "src/styles.scss" 35 | ], 36 | "scripts": [ 37 | "node_modules/paper/dist/paper-full.js" 38 | ] 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "namedChunks": false, 52 | "extractLicenses": true, 53 | "vendorChunk": false, 54 | "buildOptimizer": true, 55 | "budgets": [ 56 | { 57 | "type": "initial", 58 | "maximumWarning": "500kb", 59 | "maximumError": "1mb" 60 | }, 61 | { 62 | "type": "anyComponentStyle", 63 | "maximumWarning": "2kb", 64 | "maximumError": "4kb" 65 | } 66 | ] 67 | } 68 | } 69 | }, 70 | "serve": { 71 | "builder": "@angular-devkit/build-angular:dev-server", 72 | "options": { 73 | "browserTarget": "finitebot:build" 74 | }, 75 | "configurations": { 76 | "production": { 77 | "browserTarget": "finitebot:build:production" 78 | } 79 | } 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "finitebot:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "main": "src/test.ts", 91 | "polyfills": "src/polyfills.ts", 92 | "tsConfig": "tsconfig.spec.json", 93 | "karmaConfig": "karma.conf.js", 94 | "assets": [ 95 | "src/favicon.ico", 96 | "src/assets" 97 | ], 98 | "styles": [ 99 | "src/styles.scss" 100 | ], 101 | "scripts": [] 102 | } 103 | }, 104 | "lint": { 105 | "builder": "@angular-devkit/build-angular:tslint", 106 | "options": { 107 | "tsConfig": [ 108 | "tsconfig.app.json", 109 | "tsconfig.spec.json", 110 | "e2e/tsconfig.json" 111 | ], 112 | "exclude": [ 113 | "**/node_modules/**" 114 | ] 115 | } 116 | }, 117 | "e2e": { 118 | "builder": "@angular-devkit/build-angular:protractor", 119 | "options": { 120 | "protractorConfig": "e2e/protractor.conf.js", 121 | "devServerTarget": "finitebot:serve" 122 | }, 123 | "configurations": { 124 | "production": { 125 | "devServerTarget": "finitebot:serve:production" 126 | } 127 | } 128 | } 129 | } 130 | } 131 | }, 132 | "defaultProject": "finitebot" 133 | } 134 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('finitebot app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, './coverage/finitebot'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['Chrome'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "finitebot", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --poll 1000", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^11.1.2", 15 | "@angular/common": "~11.1.1", 16 | "@angular/compiler": "~11.1.1", 17 | "@angular/core": "~11.1.1", 18 | "@angular/forms": "~11.1.1", 19 | "@angular/platform-browser": "~11.1.1", 20 | "@angular/platform-browser-dynamic": "~11.1.1", 21 | "@angular/router": "~11.1.1", 22 | "@types/chroma-js": "^2.1.3", 23 | "chroma-js": "^2.1.2", 24 | "comfy.js": "^1.1.6", 25 | "fast-mersenne-twister": "^1.0.3", 26 | "obs-websocket-js": "^4.0.2", 27 | "paper": "^0.12.12", 28 | "rxjs": "~6.6.0", 29 | "tslib": "^2.0.0", 30 | "vara": "^1.2.0", 31 | "zone.js": "~0.11.3" 32 | }, 33 | "devDependencies": { 34 | "@angular-devkit/build-angular": "~0.1101.2", 35 | "@angular/cli": "~11.1.2", 36 | "@angular/compiler-cli": "~11.1.1", 37 | "@types/jasmine": "~3.6.0", 38 | "@types/node": "^12.11.1", 39 | "@types/tmi.js": "^1.7.1", 40 | "@types/vara": "^1.1.0", 41 | "codelyzer": "^6.0.0", 42 | "jasmine-core": "~3.6.0", 43 | "jasmine-spec-reporter": "~5.0.0", 44 | "karma": "~5.2.0", 45 | "karma-chrome-launcher": "~3.1.0", 46 | "karma-coverage": "~2.0.3", 47 | "karma-jasmine": "~4.0.0", 48 | "karma-jasmine-html-reporter": "^1.5.0", 49 | "protractor": "~7.0.0", 50 | "ts-node": "~8.3.0", 51 | "tslint": "~6.1.0", 52 | "typescript": "~4.1.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | 4 | const routes: Routes = [ 5 | { 6 | path: 'chalkboard-overlay', 7 | loadChildren: () => 8 | import('./features/chalkboard-overlay/chalkboard-overlay.module').then( 9 | (mod) => mod.ChalkboardOverlayModule 10 | ), 11 | }, 12 | { 13 | path: 'general-overlay', 14 | loadChildren: () => 15 | import('./features/general-overlay/general-overlay.module').then( 16 | (mod) => mod.GeneralOverlayModule 17 | ), 18 | }, 19 | { 20 | path: 'chalk-boom-overlay', 21 | loadChildren: () => 22 | import('./features/chalk-boom-overlay/chalk-boom-overlay.module').then( 23 | (mod) => mod.ChalkBoomOverlayModule 24 | ), 25 | }, 26 | { 27 | path: 'toggle-channel-points', 28 | loadChildren: () => 29 | import( 30 | './features/toggle-channel-points/toggle-channel-points.module' 31 | ).then((mod) => mod.ToggleChannelPointsModule), 32 | }, 33 | { 34 | path: 'circles', 35 | loadChildren: () => 36 | import('./features/circles/circles.module').then( 37 | (mod) => mod.CirclesModule 38 | ), 39 | }, 40 | { 41 | path: 'circles-binning', 42 | loadChildren: () => 43 | import('./features/circles-binning/circles-binning.module').then( 44 | (mod) => mod.CirclesBinningModule 45 | ), 46 | }, 47 | ]; 48 | 49 | @NgModule({ 50 | imports: [RouterModule.forRoot(routes)], 51 | exports: [RouterModule], 52 | }) 53 | export class AppRoutingModule {} 54 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/app.component.scss -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'finitebot'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('finitebot'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('finitebot app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'finitebot'; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { AppRoutingModule } from './app-routing.module'; 7 | import { AppComponent } from './app.component'; 8 | import { CirclesBinningComponent } from './features/circles-binning/circles-binning.component'; 9 | 10 | @NgModule({ 11 | declarations: [AppComponent, CirclesBinningComponent], 12 | imports: [ 13 | BrowserModule, 14 | BrowserAnimationsModule, 15 | HttpClientModule, 16 | AppRoutingModule, 17 | ], 18 | providers: [], 19 | bootstrap: [AppComponent], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | import { ChalkBoomOverlayComponent } from "./chalk-boom-overlay.component"; 3 | 4 | export const ChalkBoomOverlayRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: ChalkBoomOverlayComponent 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay.component.html: -------------------------------------------------------------------------------- 1 | 2 |

Type !me in chat to play.

3 |
    4 |
  • {{ player.name }}
  • 5 |
6 |
7 | 8 | 9 | 10 | 17 | 27 | 28 | 29 | 30 |
{{ winner() }} WON!!!!
31 |
32 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | color: #e9d5c8; 3 | font-family: 'Architects Daughter'; 4 | 5 | h1 { 6 | width: 100%; 7 | text-align: center; 8 | font-weight: normal; 9 | font-size: 42px; 10 | span { 11 | font-family: monospace; 12 | } 13 | } 14 | 15 | div.ball { 16 | position: absolute; 17 | width: 24px; 18 | height: 24px; 19 | border-radius: 12px; 20 | background-color: #a3d7ff; 21 | mask-image: url(/assets/brush-mask-1.png); 22 | mask-position: center center; 23 | z-index: 100000; 24 | } 25 | 26 | div.boom-container { 27 | position: absolute; 28 | width: 512px; 29 | height: 512px; 30 | display: grid; 31 | } 32 | 33 | div.boom { 34 | margin: auto; 35 | width: 64px; 36 | height: 64px; 37 | border-radius: 32px; 38 | background-color: #ff0000; 39 | } 40 | 41 | .score { 42 | position: relative; 43 | z-index: 9999; 44 | } 45 | 46 | div.winner { 47 | position:absolute; 48 | top:50%; 49 | transform:translateY(-50%), translateX(-50%); 50 | left:50%; 51 | font-size: 80px; 52 | } 53 | 54 | .player-list { 55 | list-style: none; 56 | margin-left: 0; 57 | padding-left: 1em; 58 | li:before { 59 | display: inline-block; 60 | content: "-"; 61 | width: 1em; 62 | margin-left: -1em; 63 | } 64 | li { 65 | font-size: 36px; 66 | margin-left: 100px; 67 | } 68 | } 69 | 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkBoomOverlayComponent } from './chalk-boom-overlay.component'; 4 | 5 | describe('ChalkBoomOverlayComponent', () => { 6 | let component: ChalkBoomOverlayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkBoomOverlayComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkBoomOverlayComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectorRef, 3 | Component, 4 | ElementRef, 5 | OnInit, 6 | ViewChild, 7 | ɵɵsetComponentScope, 8 | } from '@angular/core'; 9 | import ComfyJS from 'comfy.js'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | keyframes, 18 | } from '@angular/animations'; 19 | import { Point } from 'src/app/models/point'; 20 | import { ChalkBoomService, PlayerData } from './services/chalk-boom.service'; 21 | import { Circle } from 'src/app/models/circle'; 22 | import { BoundingBox } from 'src/app/models/bounding-box'; 23 | 24 | interface Teams { 25 | [key: string]: string[]; 26 | } 27 | 28 | interface Player { 29 | name: string; 30 | team: string; 31 | } 32 | 33 | function shuffleArray(array: any[]) { 34 | for (let i = array.length - 1; i > 0; i--) { 35 | const j = Math.floor(Math.random() * (i + 1)); 36 | [array[i], array[j]] = [array[j], array[i]]; 37 | } 38 | } 39 | 40 | export interface TeamData { 41 | chalk: string; 42 | player: string; 43 | name: string; 44 | } 45 | 46 | const TeamColors: { [key: string]: TeamData } = { 47 | Red: { 48 | chalk: '#FF0000', 49 | player: '#ffcccc', 50 | name: 'Red', 51 | }, 52 | Blue: { 53 | chalk: '#0066ff', 54 | player: '#66ccff', 55 | name: 'Blue', 56 | }, 57 | }; 58 | 59 | export interface BoomData { 60 | position: Point; 61 | color: string; 62 | radius: number; 63 | currentRadius: number; 64 | overlap: BoomData[]; 65 | player: PlayerData; 66 | } 67 | 68 | @Component({ 69 | selector: 'app-chalk-boom-overlay', 70 | templateUrl: './chalk-boom-overlay.component.html', 71 | styleUrls: ['./chalk-boom-overlay.component.scss'], 72 | animations: [ 73 | trigger('openClose', [ 74 | state( 75 | 'open', 76 | style({ 77 | width: '128px', 78 | height: '128px', 79 | 'border-radius': '64px', 80 | }) 81 | ), 82 | state( 83 | 'closed', 84 | style({ 85 | width: '0px', 86 | height: '0px', 87 | 'border-radius': '64px', 88 | }) 89 | ), 90 | transition('open => closed', [animate('1s')]), 91 | transition('closed => open', [animate('1s')]), 92 | ]), 93 | ], 94 | }) 95 | export class ChalkBoomOverlayComponent implements OnInit { 96 | @ViewChild('ball', { static: true }) ball?: ElementRef; 97 | 98 | phase = 'startup'; 99 | gameTime = 90000; 100 | startupTime = 20000; 101 | elapsedTime = 0; 102 | startTime = 0; 103 | booms: BoomData[] = []; 104 | lastRender = 0; 105 | speed = 450; // px/sec 106 | boomAcc = 0.025; // acc per second 107 | angle = 0.5 * Math.PI; 108 | boom = false; 109 | fps = 0; 110 | scores: { [key: string]: number } = {}; 111 | 112 | boomRate = 15; 113 | 114 | progress = 0; 115 | 116 | players: Player[] = []; 117 | teams: Teams = { 118 | red: [], 119 | blue: [], 120 | }; 121 | 122 | teamCounts = { 123 | red: 0, 124 | blue: 0, 125 | }; 126 | 127 | position: Point = { x: 0, y: 0 }; 128 | boomPosition: Point = { x: 0, y: 0 }; 129 | xDir = 1; 130 | yDir = 1; 131 | 132 | spriteWidth = 24; 133 | spriteHeight = 24; 134 | 135 | constructor( 136 | private cdr: ChangeDetectorRef, 137 | private chalkBoom: ChalkBoomService 138 | ) {} 139 | 140 | ngOnInit(): void { 141 | console.log('boom overlay component ngoninit'); 142 | setTimeout(() => { 143 | this.gameInit(); 144 | }, this.startupTime); 145 | this.chatInit(); 146 | this.startup(); 147 | } 148 | 149 | startup() {} 150 | 151 | gameInit(): void { 152 | this.phase = 'playing'; 153 | shuffleArray(this.players); 154 | this.players.forEach((player, index) => { 155 | if (index % 2 === 0) { 156 | player.team = 'Red'; 157 | } else { 158 | player.team = 'Blue'; 159 | } 160 | }); 161 | 162 | window.requestAnimationFrame((t) => this.startGame(t)); 163 | } 164 | 165 | chatInit(): void { 166 | ComfyJS.onCommand = (user, command, message, flags, extra) => { 167 | if (command === 'boom') { 168 | const playerData = this.chalkBoom.getPlayer(user); 169 | if (playerData) { 170 | const radius = playerData.boomAmount * 128; 171 | const curTeam = playerData.teamName.toLowerCase(); 172 | const otherTeam = curTeam === 'red' ? 'blue' : 'red'; 173 | const teamRatio = 174 | this.teamCounts[curTeam] / this.teamCounts[otherTeam]; 175 | const scaledRadius = Math.sqrt( 176 | Math.min(1.0, 1.0 / teamRatio) * radius ** 2 177 | ); 178 | const boomData: BoomData = { 179 | position: { ...playerData.position }, 180 | color: playerData.boomColor, 181 | radius: scaledRadius, 182 | overlap: [], 183 | player: playerData, 184 | currentRadius: 0, 185 | }; 186 | 187 | const underlying = this.booms.filter((boom) => { 188 | const rSum = boom.radius + playerData.boomAmount * 128; 189 | const d2 = 190 | (boom.position.x - playerData.position.x) ** 2 + 191 | (boom.position.y - playerData.position.y) ** 2; 192 | return d2 < rSum ** 2; 193 | }); 194 | this.booms.push(boomData); 195 | underlying.forEach((boom) => { 196 | boom.overlap.push(boomData); 197 | }); 198 | this.chalkBoom.updatePlayer({ 199 | ...playerData, 200 | boomAmount: 0, 201 | }); 202 | this.cdr.detectChanges(); 203 | } 204 | } else if (command === 'me' && this.phase === 'startup') { 205 | if (!this.players.find((player) => player.name === user)) { 206 | this.players.push({ name: user, team: '' }); 207 | } 208 | } else if (command === 'me') { 209 | if (!this.players.find((player) => player.name === user)) { 210 | const teamCounts = this.players.reduce( 211 | (acc, player) => { 212 | acc[player.team] += 1; 213 | return acc; 214 | }, 215 | { Red: 0, Blue: 0 } 216 | ); 217 | const team = teamCounts.Red > teamCounts.Blue ? 'Blue' : 'Red'; 218 | this.players.push({ name: user, team }); 219 | this.teamCounts[team.toLowerCase()] += 1; 220 | if (!(team in this.scores)) { 221 | this.scores[team] = 0; 222 | } 223 | } 224 | } 225 | }; 226 | ComfyJS.Init('FiniteSingularity'); 227 | } 228 | 229 | randomizeStart(): void { 230 | const width = Math.random() * (window.innerWidth - this.spriteWidth); 231 | const height = Math.random() * (window.innerHeight - this.spriteHeight); 232 | this.angle = 2.0 * Math.PI * Math.random(); 233 | this.position = { x: width / 2, y: height / 2 }; 234 | if (this.angle >= Math.PI / 2.0 && this.angle < (3.0 * Math.PI) / 2.0) { 235 | this.xDir = -1; 236 | } else { 237 | this.xDir = 1; 238 | } 239 | 240 | if (this.angle <= Math.PI) { 241 | this.yDir = -1; 242 | } else { 243 | this.yDir = 1; 244 | } 245 | } 246 | 247 | startGame(timestamp: number): void { 248 | this.elapsedTime = timestamp; 249 | this.lastRender = timestamp; 250 | this.startTime = this.elapsedTime; 251 | Object.keys(this.chalkBoom.players).forEach((key) => { 252 | const player = this.chalkBoom.players[key]; 253 | this.scores[player.teamName] = 0; 254 | this.teamCounts[player.teamName.toLowerCase()] += 1; 255 | }); 256 | window.requestAnimationFrame((t) => this.gameLoop(t)); 257 | } 258 | 259 | gameLoop(timestamp: number): void { 260 | this.progress = timestamp - this.lastRender; 261 | this.fps = 1.0 / (this.progress / 1000.0); 262 | this.update(this.progress); 263 | this.elapsedTime = timestamp; 264 | this.lastRender = timestamp; 265 | if (this.gameTime - (this.elapsedTime - this.startTime) > 0) { 266 | window.requestAnimationFrame((t) => this.gameLoop(t)); 267 | } else { 268 | this.phase = 'winner'; 269 | } 270 | } 271 | 272 | winner() { 273 | let highScore = -1; 274 | let winner = 'na'; 275 | Object.keys(this.scores).forEach((key) => { 276 | if (this.scores[key] > highScore) { 277 | winner = key; 278 | highScore = this.scores[key]; 279 | } 280 | }); 281 | return winner; 282 | } 283 | 284 | update(progress: number): void { 285 | this.chalkBoom.updateBoomAmmount((progress / 1000.0) * this.boomAcc); 286 | this.calculateScore(); 287 | } 288 | 289 | teamData(player: Player): any { 290 | return TeamColors[player.team]; 291 | } 292 | 293 | calculateScore(): void { 294 | Object.keys(this.scores).forEach((key) => { 295 | this.scores[key] = 0; 296 | }); 297 | this.booms.forEach((boom) => { 298 | const score = this.calculateBoomContribution(boom); 299 | this.scores[boom.player.teamName] += score; 300 | }); 301 | 302 | Object.keys(this.scores).forEach((key) => { 303 | this.scores[key] /= window.innerHeight * window.innerWidth; 304 | }); 305 | this.scores = { ...this.scores }; 306 | } 307 | 308 | calculateBoomContribution(boom: BoomData): number { 309 | const circle: Circle = { 310 | center: boom.position, 311 | r: boom.currentRadius, 312 | color: null, 313 | }; 314 | const overlapCircles = boom.overlap.map((overlapBoom) => { 315 | return { 316 | center: overlapBoom.position, 317 | r: overlapBoom.currentRadius, 318 | color: null, 319 | }; 320 | }); 321 | const compoundArea = this.estimateCompoundArea(circle, overlapCircles); 322 | return compoundArea; 323 | } 324 | 325 | estimateCompoundArea( 326 | circle: Circle, 327 | overlapCircles: Circle[], 328 | binSize = 1 329 | ): number { 330 | let area = 0; 331 | const bb = this.getBoundingBox(circle); 332 | for (let y = bb.minBound.y; y <= bb.maxBound.y; y += binSize) { 333 | let checked = false; 334 | let cBounds = this.scanLineBounds(circle, y); 335 | let scanLineArea = (cBounds.xMax - cBounds.xMin) * binSize; 336 | if (isNaN(scanLineArea)) { 337 | continue; 338 | } 339 | const scanLineOverlap = overlapCircles.filter((c) => { 340 | return c.center.y - c.r <= y && c.center.y + c.r >= y; 341 | }); 342 | let overlapBounds: { xMin: number; xMax: number }[] = []; 343 | scanLineOverlap.forEach((slo) => { 344 | const bounds = this.scanLineBounds(slo, y); 345 | if (bounds.xMin < cBounds.xMax && bounds.xMax > cBounds.xMin) { 346 | bounds.xMin = Math.max(bounds.xMin, cBounds.xMin); 347 | bounds.xMax = Math.min(bounds.xMax, cBounds.xMax); 348 | overlapBounds.push({ ...bounds }); 349 | } 350 | }); 351 | overlapBounds = overlapBounds.sort((a, b) => (a.xMin < b.xMin ? -1 : 1)); 352 | 353 | const constrainedBounds = []; 354 | overlapBounds.forEach((bounds) => { 355 | const length = constrainedBounds.length; 356 | if (length === 0) { 357 | constrainedBounds.push({ ...bounds }); 358 | } else if (bounds.xMin <= constrainedBounds[length - 1].xMax) { 359 | constrainedBounds[length - 1].xMax = bounds.xMax; 360 | } else { 361 | constrainedBounds.push({ ...bounds }); 362 | } 363 | }); 364 | constrainedBounds.forEach((bounds) => { 365 | scanLineArea -= bounds.xMax - bounds.xMin; 366 | }); 367 | area += scanLineArea; 368 | } 369 | 370 | return area; 371 | } 372 | 373 | scanLineBounds(circle: Circle, y: number) { 374 | const c = Math.sqrt(circle.r ** 2 - (y - circle.center.y) ** 2); 375 | const xMin = circle.center.x - c; 376 | const xMax = circle.center.x + c; 377 | return { xMin, xMax }; 378 | } 379 | 380 | getBoundingBox(circle: Circle): BoundingBox { 381 | return { 382 | minBound: { 383 | x: circle.center.x - circle.r, 384 | y: circle.center.y - circle.r, 385 | }, 386 | maxBound: { 387 | x: circle.center.x + circle.r, 388 | y: circle.center.y + circle.r, 389 | }, 390 | }; 391 | } 392 | 393 | currentRadius(value: number, index: number): void { 394 | this.booms[index].currentRadius = value; 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/chalk-boom-overlay.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { ChalkBoomOverlayComponent } from './chalk-boom-overlay.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ChalkBoomOverlayRoutes } from './chalk-boom-overlay-routing.module'; 6 | import { BoomComponent } from './components/boom/boom.component'; 7 | import { TimerComponent } from './components/timer/timer.component'; 8 | import { RocketComponent } from './components/rocket/rocket.component'; 9 | import { ScoreComponent } from './components/score/score.component'; 10 | 11 | @NgModule({ 12 | declarations: [ 13 | ChalkBoomOverlayComponent, 14 | BoomComponent, 15 | TimerComponent, 16 | RocketComponent, 17 | ScoreComponent, 18 | ], 19 | imports: [ 20 | CommonModule, 21 | RouterModule.forChild(ChalkBoomOverlayRoutes) 22 | ] 23 | }) 24 | export class ChalkBoomOverlayModule { } 25 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/boom/boom.component.html: -------------------------------------------------------------------------------- 1 |
5 |
14 |
15 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/boom/boom.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | } 9 | 10 | div.boom-container { 11 | position: absolute; 12 | width: 512px; 13 | height: 512px; 14 | display: grid; 15 | } 16 | 17 | div.boom { 18 | margin: auto; 19 | width: 64px; 20 | height: 64px; 21 | border-radius: 32px; 22 | background-color: #ff0000; 23 | mask-image: url(/assets/brush-mask-1.png); 24 | mask-position: center center; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/boom/boom.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BoomComponent } from './boom.component'; 4 | 5 | describe('BoomComponent', () => { 6 | let component: BoomComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BoomComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BoomComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/boom/boom.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; 2 | import { Point } from 'src/app/models/point'; 3 | 4 | @Component({ 5 | selector: 'app-boom', 6 | templateUrl: './boom.component.html', 7 | styleUrls: ['./boom.component.scss'], 8 | }) 9 | export class BoomComponent implements OnInit, AfterViewInit, OnChanges { 10 | @Input() position: Point = { x: 0, y: 0 }; 11 | @Input() color: string = '#FFFFFF'; 12 | @Input() zIndex: number = 0; 13 | @Input() progress: number = 0; 14 | @Input() maxRadius: number = 24; 15 | @Input() boomRate: number = 15; 16 | @Output() currentRadius = new EventEmitter(); 17 | 18 | maxBoomRadius = 0; 19 | boom = false; 20 | radius = 0; 21 | 22 | constructor() { } 23 | 24 | ngOnInit(): void { 25 | 26 | } 27 | 28 | ngOnChanges(simpleChanges: SimpleChanges): void { 29 | if ('maxRadius' in simpleChanges && simpleChanges.maxRadius.firstChange) { 30 | this.maxBoomRadius = this.maxRadius; 31 | } 32 | this.setBoomRadius(); 33 | } 34 | 35 | ngAfterViewInit(): void { 36 | this.boom = true; 37 | } 38 | 39 | setBoomRadius(): void { 40 | if (this.progress && this.boomRate && this.maxBoomRadius) { 41 | const boomIncrement = (this.progress / 1000.0 * this.boomRate); 42 | this.radius = Math.min( 43 | this.radius + boomIncrement, 44 | this.maxRadius 45 | ); 46 | this.currentRadius.emit(this.radius); 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/rocket/rocket.component.html: -------------------------------------------------------------------------------- 1 |
9 |
10 |
18 | {{ data.name }} 19 |
25 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/rocket/rocket.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: absolute; 4 | top: 0; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | font-family: 'Architects Daughter'; 9 | } 10 | 11 | .rocket { 12 | position: absolute; 13 | left: 0; 14 | top: 0; 15 | width: 24px; 16 | height: 24px; 17 | border-radius: 12px; 18 | mask-image: url(/assets/brush-mask-1.png); 19 | mask-position: center center; 20 | z-index: 100000; 21 | overflow: visible; 22 | } 23 | 24 | .name { 25 | position: absolute; 26 | width: 300px; 27 | text-align: center; 28 | font-size: 32px; 29 | font-weight: bold; 30 | z-index: 100000; 31 | 32 | .boom-meter-holder { 33 | border-style: solid; 34 | border-width: 3px; 35 | width: 100%; 36 | height: 8px; 37 | 38 | .boom-amount { 39 | width: 0%; 40 | height: 100%; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/rocket/rocket.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RocketComponent } from './rocket.component'; 4 | 5 | describe('RocketComponent', () => { 6 | let component: RocketComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ RocketComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RocketComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/rocket/rocket.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; 2 | import { Point } from 'src/app/models/point'; 3 | import { TeamData } from '../../chalk-boom-overlay.component'; 4 | import { ChalkBoomService, PlayerData } from '../../services/chalk-boom.service'; 5 | 6 | @Component({ 7 | selector: 'app-rocket', 8 | templateUrl: './rocket.component.html', 9 | styleUrls: ['./rocket.component.scss'] 10 | }) 11 | export class RocketComponent implements OnInit, OnChanges { 12 | @Input() progress = 0; 13 | @Input() speed = 100; 14 | @Input() player = ''; 15 | @Input() teamData: TeamData = { 16 | chalk: '#FFFFFF', 17 | player: '#FFFFFF', 18 | name: 'team', 19 | }; 20 | setup = false; 21 | 22 | data: PlayerData = { 23 | name: '', 24 | boomAmount: 0, 25 | position: {x: 0, y: 0}, 26 | angle: 0, 27 | xDir: 1, 28 | yDir: 1, 29 | spriteWidth: 24, 30 | spriteHeight: 24, 31 | teamColor: '', 32 | boomColor: '', 33 | teamName: '', 34 | } 35 | boomAmount = 0.5; 36 | 37 | constructor( 38 | private chalkBoom: ChalkBoomService 39 | ) { } 40 | 41 | ngOnInit(): void { 42 | this.data.name = this.player; 43 | this.data.teamColor = this.teamData.player; 44 | this.data.boomColor = this.teamData.chalk; 45 | this.data.teamName = this.teamData.name; 46 | this.data.boomAmount = 0.5; 47 | this.randomizeStart(); 48 | this.setup = true; 49 | } 50 | 51 | ngOnChanges(simpleChanges: SimpleChanges) { 52 | if(this.setup) 53 | this.update(); 54 | } 55 | 56 | randomizeStart(): void { 57 | const width = Math.random() * (window.innerWidth - this.data.spriteWidth); 58 | const height = Math.random() * (window.innerHeight - this.data.spriteHeight); 59 | this.data.angle = 2.0 * Math.PI * Math.random(); 60 | this.data.position = {x: width/2, y: height/2}; 61 | if(this.data.angle >= Math.PI/2.0 && this.data.angle < 3.0*Math.PI/2.0) { 62 | this.data.xDir = -1; 63 | } else { 64 | this.data.xDir = 1; 65 | } 66 | 67 | if(this.data.angle <= Math.PI) { 68 | this.data.yDir = -1; 69 | } else { 70 | this.data.yDir = 1; 71 | } 72 | 73 | this.chalkBoom.addPlayer({ 74 | ...this.data 75 | }); 76 | } 77 | 78 | updatePlayer(): void { 79 | this.chalkBoom.updatePlayer(this.data); 80 | } 81 | 82 | update(): void { 83 | this.data = this.chalkBoom.getPlayer(this.data.name); 84 | const width = window.innerWidth - this.data.spriteWidth; 85 | const height = window.innerHeight - this.data.spriteHeight; 86 | 87 | const distance = this.progress/1000.0 * this.speed; 88 | 89 | let newX = this.data.position.x; 90 | if(Math.abs(Math.cos(this.data.angle)) > 1e-8) { 91 | newX = this.data.position.x + distance * Math.cos(this.data.angle); 92 | } 93 | let newY = this.data.position.y; 94 | if(Math.abs(Math.sin(this.data.angle)) > 1e-8) { 95 | newY = this.data.position.y - distance * Math.sin(this.data.angle); 96 | } 97 | 98 | const angleChange = newX > width || newX < 0 || newY > height || newY < 0; 99 | 100 | if(newX > width) { 101 | const xDiff = newX - width; 102 | newX = width-xDiff; 103 | if(this.data.yDir > 0) { 104 | const theta = this.data.angle - 3.0*Math.PI/2.0; 105 | this.data.angle = 3.0*Math.PI/2.0 - theta; 106 | } else { 107 | const theta = Math.PI/2.0 - this.data.angle; 108 | this.data.angle = Math.PI/2.0 + theta; 109 | } 110 | this.data.xDir = -1; 111 | } else if(newX < 0) { 112 | const xDiff = -newX; 113 | newX = xDiff; 114 | if(this.data.yDir > 0) { 115 | const theta = 3.0*Math.PI/2.0-this.data.angle; 116 | this.data.angle = 3.0*Math.PI/2.0 + theta; 117 | } else { 118 | const theta = this.data.angle - Math.PI/2.0; 119 | this.data.angle = Math.PI/2.0 - theta; 120 | } 121 | this.data.xDir = 1; 122 | } 123 | 124 | if(newY > height) { 125 | const yDiff = newY - height; 126 | newY = height - yDiff; 127 | if(this.data.xDir > 0) { 128 | const theta = 2.0*Math.PI - this.data.angle; 129 | this.data.angle = theta; 130 | } else { 131 | const theta = this.data.angle - Math.PI; 132 | this.data.angle = Math.PI - theta; 133 | } 134 | this.data.yDir = -1; 135 | } else if(newY < 0) { 136 | const yDiff = -newY; 137 | newY = yDiff; 138 | if(this.data.xDir > 0) { 139 | const theta = this.data.angle; 140 | this.data.angle = 2.0*Math.PI - theta; 141 | } else { 142 | const theta = Math.PI - this.data.angle; 143 | this.data.angle = Math.PI + theta; 144 | } 145 | this.data.yDir = 1; 146 | } 147 | 148 | if(angleChange) { 149 | const randomJitter = Math.random() * Math.PI/(9.0); 150 | this.data.angle += Math.sign(this.data.angle) * randomJitter; 151 | } 152 | 153 | this.data.position = {x: newX, y: newY}; 154 | this.updatePlayer(); 155 | // console.log(this.position); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/score/score.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{ key }}
3 | {{scores[key] * 100 | number: '1.1-1' }} 4 |
5 | 6 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/score/score.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | position: absolute; 4 | top: 10px; 5 | left: 10px; 6 | z-index: 9999; 7 | color: #FFFFFF; 8 | font-family: 'Architects Daughter'; 9 | font-size: 46px; 10 | 11 | div { 12 | width: 200px; 13 | float: right; 14 | text-align: center; 15 | &:first-of-type { 16 | float: left; 17 | } 18 | span { 19 | text-decoration: underline; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/score/score.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ScoreComponent } from './score.component'; 4 | 5 | describe('ScoreComponent', () => { 6 | let component: ScoreComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ScoreComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ScoreComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/score/score.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-score', 5 | templateUrl: './score.component.html', 6 | styleUrls: ['./score.component.scss'] 7 | }) 8 | export class ScoreComponent implements OnInit, OnChanges { 9 | @Input() scores: {[key: string]: number} = {}; 10 | keys: string[] = []; 11 | constructor() { } 12 | 13 | ngOnInit(): void { 14 | 15 | } 16 | 17 | ngOnChanges(simpleChanges: SimpleChanges) { 18 | this.keys = Object.keys(this.scores); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/timer/timer.component.html: -------------------------------------------------------------------------------- 1 |

{{ (gameTime-timeElapsed)/1000 | number:'1.0-0' }}

2 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/timer/timer.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: block; 3 | width: 100%; 4 | position: relative; 5 | z-index: 9999; 6 | font-family: 'Architects Daughter'; 7 | h1 { 8 | width: 100%; 9 | text-align: center; 10 | margin-top: 40px; 11 | font-size: 48px; 12 | color: #ffffff; 13 | } 14 | } 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/timer/timer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { TimerComponent } from './timer.component'; 4 | 5 | describe('TimerComponent', () => { 6 | let component: TimerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ TimerComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(TimerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/components/timer/timer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-timer', 5 | templateUrl: './timer.component.html', 6 | styleUrls: ['./timer.component.scss'] 7 | }) 8 | export class TimerComponent implements OnInit { 9 | @Input() gameTime = 0; 10 | @Input() timeElapsed = 0; 11 | constructor() { } 12 | 13 | ngOnInit(): void { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/services/chalk-boom.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkBoomService } from './chalk-boom.service'; 4 | 5 | describe('ChalkBoomService', () => { 6 | let service: ChalkBoomService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ChalkBoomService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/features/chalk-boom-overlay/services/chalk-boom.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Point } from 'src/app/models/point'; 3 | 4 | export interface PlayerData { 5 | position: Point; 6 | boomAmount: number; 7 | name: string; 8 | angle: number; 9 | xDir: number; 10 | yDir: number; 11 | spriteWidth: number; 12 | spriteHeight: number; 13 | teamColor: string; 14 | boomColor: string; 15 | teamName: string; 16 | } 17 | 18 | @Injectable({ 19 | providedIn: 'root' 20 | }) 21 | export class ChalkBoomService { 22 | players: {[key: string]: PlayerData} = {}; 23 | 24 | constructor() { } 25 | 26 | addPlayer(data: PlayerData) { 27 | if(!(data.name in this.players)) { 28 | this.players[data.name] = { 29 | ...data 30 | }; 31 | } 32 | } 33 | 34 | getPlayer(name: string): PlayerData { 35 | return { ...this.players[name] }; 36 | } 37 | 38 | updatePlayer(data: PlayerData) { 39 | if(data.name in this.players) { 40 | this.players[data.name] = {...data}; 41 | } 42 | } 43 | 44 | updateBoomAmmount(additionalBoom: number) { 45 | for(const key of Object.keys(this.players)) { 46 | const newBoom = this.players[key].boomAmount + additionalBoom; 47 | this.players[key].boomAmount = Math.min(newBoom, 1.0); 48 | } 49 | } 50 | 51 | clearPlayers() { 52 | this.players = {}; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | import { ChalkboardOverlayComponent } from "./chalkboard-overlay.component"; 3 | 4 | export const ChalkboardOverlayRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: ChalkboardOverlayComponent 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay.component.html: -------------------------------------------------------------------------------- 1 | 7 | 13 | 19 | 25 | 31 | 38 | 43 | 50 | 57 | 58 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/chalkboard-overlay.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardOverlayComponent } from './chalkboard-overlay.component'; 4 | 5 | describe('ChalkboardOverlayComponent', () => { 6 | let component: ChalkboardOverlayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardOverlayComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardOverlayComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | import { 4 | ObsWebsocketsService, 5 | stopAdvKeys, 6 | startAdvKeys, 7 | } from 'src/app/services/obs-websockets.service'; 8 | import OBSWebSocket from 'obs-websocket-js'; 9 | import { 10 | TwitchEventsService, 11 | TwitchEvent, 12 | } from 'src/app/services/twitch-events.service'; 13 | import { HttpClient } from '@angular/common/http'; 14 | import { EmoteMessage, MatrixMessageRow } from 'src/app/models/emote-message'; 15 | import { MersenneTwister } from 'fast-mersenne-twister'; 16 | @Component({ 17 | selector: 'app-chalkboard-overlay', 18 | templateUrl: './chalkboard-overlay.component.html', 19 | styleUrls: ['./chalkboard-overlay.component.scss'], 20 | }) 21 | export class ChalkboardOverlayComponent implements OnInit { 22 | private subs = new Subscription(); 23 | obs = new OBSWebSocket(); 24 | 25 | currentScene: string = ''; 26 | currentOverlay: string = ''; 27 | currentMessage: any; 28 | currentExtraData: any; 29 | 30 | currentScene$: string = ''; 31 | 32 | obsReady = false; 33 | logMsg = ''; 34 | lastBehindYou = 0; 35 | 36 | eventQueue: TwitchEvent[] = []; 37 | 38 | lastScenes = { 39 | captJackReview: [], 40 | }; 41 | 42 | constructor( 43 | private twitchEvents: TwitchEventsService, 44 | private obsWs: ObsWebsocketsService, 45 | private cdr: ChangeDetectorRef, 46 | private http: HttpClient 47 | ) {} 48 | 49 | ngOnInit(): void { 50 | this.twitchEvents.connect(); 51 | this.obsWs.connect(); 52 | this.obs 53 | .connect({ address: 'localhost:4444' }) 54 | .then(() => { 55 | console.log('Connection to OBS!'); 56 | return; 57 | }) 58 | .then(() => { 59 | this.obsReady = true; 60 | }); 61 | 62 | this.obs.on('SwitchScenes', (data) => { 63 | this.currentScene$ = data['scene-name']; 64 | }); 65 | 66 | this.subs.add( 67 | this.twitchEvents.message.subscribe(async (msg) => { 68 | if (!msg) { 69 | return; 70 | } 71 | console.log(msg); 72 | if (msg.event_type === 'channel-cheer') { 73 | const extraData = await this.twitchEvents 74 | .getTwitchUserData(msg.event_data.user_login) 75 | .toPromise(); 76 | this.eventQueue.push({ 77 | overlay: 'cheer', 78 | eventData: msg, 79 | extraData, 80 | blur: true, 81 | }); 82 | } else if ( 83 | msg.event_type === 84 | 'channel-channel_points_custom_reward_redemption-add' && 85 | msg.event_data.reward.id === '575c2991-cbc2-402c-837f-86bd40009379' 86 | ) { 87 | console.log('write on the wall'); 88 | this.eventQueue.push({ 89 | overlay: 'point-test', 90 | eventData: msg, 91 | blur: false, 92 | }); 93 | } else if ( 94 | msg.event_type === 95 | 'channel-channel_points_custom_reward_redemption-add' && 96 | msg.event_data.reward.id === '146beb92-ed1e-4845-8604-3408ebabf411' 97 | ) { 98 | console.log('Cpt. Jack Review'); 99 | this.eventQueue.push({ 100 | overlay: 'capt-jack-review', 101 | eventData: msg, 102 | blur: false, 103 | }); 104 | } else if ( 105 | msg.event_type === 106 | 'channel-channel_points_custom_reward_redemption-add' && 107 | msg.event_data.reward.id === '99ef7c31-be4b-463f-8059-92c80f98ef32' 108 | ) { 109 | console.log('bring water!'); 110 | this.eventQueue.push({ 111 | overlay: 'bring-water', 112 | eventData: msg, 113 | blur: false, 114 | }); 115 | } else if ( 116 | msg.event_type === 117 | 'channel-channel_points_custom_reward_redemption-add' && 118 | msg.event_data.reward.id === '4fd2137c-8dd6-411b-b406-a077ce017d0f' 119 | ) { 120 | console.log('calling behind you'); 121 | this.eventQueue.push({ 122 | overlay: 'behind-you', 123 | eventData: msg, 124 | blur: false, 125 | }); 126 | } else if ( 127 | msg.event_type === 128 | 'channel-channel_points_custom_reward_redemption-add' && 129 | msg.event_data.reward.id === '7b9da1b1-eaab-4dea-afe0-cb361b36dd49' 130 | ) { 131 | console.log('write to matrix!'); 132 | const message: EmoteMessage = { 133 | text: msg.event_data.user_input, 134 | emotes: msg.event_data.user_input_emotes, 135 | }; 136 | this.writeToTheMatrix(message); 137 | } else if (msg.event_type === 'channel-subscription-message') { 138 | const username = msg.event_data.user_name; 139 | const extraData = await this.twitchEvents 140 | .getTwitchUserData(username) 141 | .toPromise(); 142 | this.eventQueue.push({ 143 | overlay: 'subscribe', 144 | eventData: msg, 145 | extraData, 146 | blur: true, 147 | }); 148 | } else if (msg.event_type === 'channel-subscription-gift') { 149 | const username = msg.event_data.user_name; 150 | const extraData = await this.twitchEvents 151 | .getTwitchUserData(username) 152 | .toPromise(); 153 | this.eventQueue.push({ 154 | overlay: 'gift-subscription', 155 | eventData: msg, 156 | extraData, 157 | blur: true, 158 | }); 159 | } else if (msg.event_type === 'channel-raid') { 160 | const extraData = await this.twitchEvents 161 | .getTwitchUserData(msg.event_data.from_broadcaster_user_name) 162 | .toPromise(); 163 | this.eventQueue.push({ 164 | overlay: 'raid', 165 | eventData: msg, 166 | extraData, 167 | blur: false, 168 | }); 169 | } 170 | console.log(msg); 171 | if (this.eventQueue.length === 1) { 172 | this.obsCheckEventQueue(true); 173 | } 174 | }) 175 | ); 176 | } 177 | 178 | writeToTheMatrix(message: EmoteMessage) { 179 | console.log(message); 180 | const emotes = message.emotes 181 | ? message.emotes.reduce((acc, val) => { 182 | const currentEmotes = val.positions.map((emotePos) => ({ 183 | id: val.id, 184 | position: emotePos, 185 | })); 186 | return [...acc, ...currentEmotes]; 187 | }, []) 188 | : []; 189 | 190 | emotes.sort((a, b) => (a.position[0] < b.position[0] ? -1 : 1)); 191 | 192 | const payload: MatrixMessageRow[] = 193 | emotes.length > 0 194 | ? emotes.reduce((acc, val, i) => { 195 | let current = []; 196 | if (i === 0 && val.position[0] > 0) { 197 | current.push({ 198 | mc_type: 'string', 199 | value: Array.from(message.text) 200 | .slice(0, val.position[0]) 201 | .join(''), 202 | }); 203 | } 204 | current.push({ 205 | mc_type: 'emote', 206 | value: val.id, 207 | }); 208 | if (i < emotes.length - 1) { 209 | const nextStart = emotes[i + 1].position[0]; 210 | const subStrLng = nextStart - val.position[1] - 1; 211 | current.push({ 212 | mc_type: 'string', 213 | value: Array.from(message.text) 214 | .slice(val.position[1] + 1, nextStart) 215 | .join(''), 216 | }); 217 | } else if (val.position[1] != message.text.length - 1) { 218 | current.push({ 219 | mc_type: 'string', 220 | value: Array.from(message.text) 221 | .slice(val.position[1] + 1) 222 | .join(''), 223 | }); 224 | } 225 | return [...acc, ...current]; 226 | }, []) 227 | : [ 228 | { 229 | mc_type: 'string', 230 | value: message.text, 231 | }, 232 | ]; 233 | this.http.post('http://192.168.1.116:5000', payload).subscribe((resp) => { 234 | console.log(resp); 235 | }); 236 | } 237 | 238 | async obsCheckEventQueue(originalEvent: boolean) { 239 | console.log('obsCheckEventQueue'); 240 | if (originalEvent && this.eventQueue.length === 1) { 241 | const event = this.eventQueue[0]; 242 | this.clearCurrentEvent(); 243 | this.obsSceneToAlert( 244 | event.eventData, 245 | event.overlay, 246 | event.extraData, 247 | event.blur 248 | ); 249 | } else if (!originalEvent && this.eventQueue.length > 0) { 250 | const event = this.eventQueue[0]; 251 | this.clearCurrentEvent(); 252 | this.obsAlert( 253 | event.eventData, 254 | event.overlay, 255 | event.extraData, 256 | event.blur 257 | ); 258 | } else if (this.eventQueue.length === 0) { 259 | this.clearCurrentEvent(); 260 | } 261 | } 262 | 263 | clearCurrentEvent() { 264 | this.currentMessage = null; 265 | this.currentOverlay = ''; 266 | this.currentExtraData = null; 267 | this.cdr.detectChanges(); 268 | } 269 | 270 | async obsSceneToAlert( 271 | msg: any, 272 | overlay: string, 273 | extraData: any, 274 | blur: boolean 275 | ) { 276 | while ( 277 | this.currentScene$ === 'Starting Soon' || 278 | this.currentScene$ === 'Chalk Boom' 279 | ) { 280 | console.log('waiting...'); 281 | await new Promise((r) => setTimeout(r, 500)); 282 | } 283 | this.obsWs.sendMessage({ 284 | 'request-type': 'TriggerHotkeyBySequence', 285 | ...stopAdvKeys, 286 | 'message-id': '12345', 287 | }); 288 | this.obs 289 | .send('GetCurrentScene') 290 | .then((data) => { 291 | this.currentScene = data.name; 292 | }) 293 | .then(() => { 294 | this.obs.send('SetCurrentScene', { 295 | 'scene-name': 'Alerts GS', 296 | }); 297 | if (blur) { 298 | setTimeout(() => { 299 | this.obs.send('SetSourceFilterVisibility', { 300 | sourceName: 'Super Composite', 301 | filterName: 'Blurry', 302 | filterEnabled: true, 303 | }); 304 | this.obs.send('SetSourceFilterVisibility', { 305 | sourceName: 'DSLR Face Cam GS For Mask', 306 | filterName: 'Blurry', 307 | filterEnabled: true, 308 | }); 309 | this.obs.send('SetSourceFilterVisibility', { 310 | sourceName: 'Office BG GS', 311 | filterName: 'Sharp', 312 | filterEnabled: true, 313 | }); 314 | }, 750); 315 | } 316 | 317 | setTimeout(() => { 318 | this.currentMessage = msg; 319 | this.currentOverlay = overlay; 320 | this.currentExtraData = extraData; 321 | }, 1500); 322 | }); 323 | } 324 | 325 | obsAlert(msg: any, overlay: string, extraData: any, blur: boolean) { 326 | this.currentMessage = msg; 327 | this.currentOverlay = overlay; 328 | this.currentExtraData = extraData; 329 | } 330 | 331 | writingComplete() { 332 | this.eventQueue.shift(); 333 | if (this.eventQueue.length > 0) { 334 | this.obsCheckEventQueue(false); 335 | return; 336 | } else { 337 | this.clearCurrentEvent(); 338 | } 339 | this.obs.send('SetCurrentScene', { 340 | 'scene-name': this.currentScene, 341 | }); 342 | this.obs.send('SetSourceFilterVisibility', { 343 | sourceName: 'Super Composite', 344 | filterName: 'Sharp', 345 | filterEnabled: true, 346 | }); 347 | this.obs.send('SetSourceFilterVisibility', { 348 | sourceName: 'DSLR Face Cam GS For Mask', 349 | filterName: 'Sharp', 350 | filterEnabled: true, 351 | }); 352 | this.obs.send('SetSourceFilterVisibility', { 353 | sourceName: 'Office BG GS', 354 | filterName: 'Blurry', 355 | filterEnabled: true, 356 | }); 357 | this.obsWs.sendMessage({ 358 | 'request-type': 'TriggerHotkeyBySequence', 359 | ...startAdvKeys, 360 | 'message-id': '54321', 361 | }); 362 | } 363 | } 364 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/chalkboard-overlay.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { ChalkboardOverlayRoutes } from './chalkboard-overlay-routing.module'; 5 | import { ChalkboardOverlayComponent } from './chalkboard-overlay.component'; 6 | import { ChalkboardCheerComponent } from './components/chalkboard-cheer/chalkboard-cheer.component'; 7 | import { ChalkboardPointTestComponent } from './components/chalkboard-point-test/chalkboard-point-test.component'; 8 | import { ChalkboardSubscribeComponent } from './components/chalkboard-subscribe/chalkboard-subscribe.component'; 9 | import { ChalkboardRaidComponent } from './components/chalkboard-raid/chalkboard-raid.component'; 10 | import { BringFiniteWaterComponent } from './components/bring-finite-water/bring-finite-water.component'; 11 | import { BehindYouComponent } from './components/behind-you/behind-you.component'; 12 | import { MessageMatrixComponent } from './components/message-matrix/message-matrix.component'; 13 | import { ChalkboardGiftSubComponent } from './components/chalkboard-gift-sub/chalkboard-gift-sub.component'; 14 | import { ChalkboardSubNoMessageComponent } from './components/chalkboard-sub-no-message/chalkboard-sub-no-message.component'; 15 | import { CaptJackReviewComponent } from './components/capt-jack-review/capt-jack-review.component'; 16 | 17 | @NgModule({ 18 | declarations: [ 19 | ChalkboardOverlayComponent, 20 | ChalkboardCheerComponent, 21 | ChalkboardPointTestComponent, 22 | ChalkboardSubscribeComponent, 23 | ChalkboardRaidComponent, 24 | BringFiniteWaterComponent, 25 | BehindYouComponent, 26 | MessageMatrixComponent, 27 | ChalkboardGiftSubComponent, 28 | ChalkboardSubNoMessageComponent, 29 | CaptJackReviewComponent, 30 | ], 31 | imports: [CommonModule, RouterModule.forChild(ChalkboardOverlayRoutes)], 32 | }) 33 | export class ChalkboardOverlayModule {} 34 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.html -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BehindYouComponent } from './behind-you.component'; 4 | 5 | describe('BehindYouComponent', () => { 6 | let component: BehindYouComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BehindYouComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BehindYouComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/behind-you/behind-you.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | import OBSWebSocket from 'obs-websocket-js'; 4 | import { MersenneTwister } from 'fast-mersenne-twister'; 5 | 6 | const videoLength = { 7 | 1: 11000, 8 | 2: 16000, 9 | 3: 29000, 10 | 4: 17500, 11 | 5: 8000, 12 | }; 13 | 14 | @Component({ 15 | selector: 'app-behind-you', 16 | templateUrl: './behind-you.component.html', 17 | styleUrls: ['./behind-you.component.scss'], 18 | }) 19 | export class BehindYouComponent implements OnInit { 20 | private subs = new Subscription(); 21 | @Output() redemptionComplete = new EventEmitter(); 22 | @Output() effectSelected = new EventEmitter(); 23 | @Input() obs: OBSWebSocket; 24 | @Input() lastEffect = 0; 25 | step = ''; 26 | constructor() {} 27 | 28 | ngOnInit(): void { 29 | console.log('behind you!'); 30 | this.behindYou(); 31 | } 32 | 33 | behindYou() { 34 | const rng = MersenneTwister(Date.now()); 35 | const effect = Math.ceil(rng.random() * 5); 36 | 37 | this.turnOnVideo(effect); 38 | setTimeout(() => { 39 | this.turnOffVideo(effect); 40 | }, videoLength[effect]); 41 | setTimeout(() => { 42 | this.redemptionComplete.emit(); 43 | this.effectSelected.emit(effect); 44 | }, videoLength[effect] + 600); 45 | } 46 | 47 | turnOnVideo(videoId: number) { 48 | this.obs.send('SetSourceFilterSettings', { 49 | sourceName: `behind-you-${videoId}`, 50 | filterName: 'Opacity', 51 | filterSettings: { 52 | opacity: 0, 53 | }, 54 | }); 55 | this.obs.send('SetSceneItemProperties', { 56 | 'scene-name': 'DSLR Face Cam Composite', 57 | item: { name: `behind-you-${videoId}` }, 58 | visible: true, 59 | position: {}, 60 | bounds: {}, 61 | scale: {}, 62 | crop: {}, 63 | }); 64 | this.obs.send('SetSourceFilterVisibility', { 65 | sourceName: `behind-you-${videoId}`, 66 | filterName: 'FadeIn', 67 | filterEnabled: true, 68 | }); 69 | this.obs.send('SetSourceFilterVisibility', { 70 | sourceName: 'Super Composite', 71 | filterName: 'Blurry', 72 | filterEnabled: true, 73 | }); 74 | this.obs.send('SetSourceFilterVisibility', { 75 | sourceName: 'DSLR Face Cam GS For Mask', 76 | filterName: 'Blurry', 77 | filterEnabled: true, 78 | }); 79 | } 80 | 81 | turnOffVideo(videoId: number) { 82 | this.obs.send('SetSourceFilterVisibility', { 83 | sourceName: `behind-you-${videoId}`, 84 | filterName: 'FadeOut', 85 | filterEnabled: true, 86 | }); 87 | this.obs.send('SetSourceFilterVisibility', { 88 | sourceName: 'Super Composite', 89 | filterName: 'Sharp', 90 | filterEnabled: true, 91 | }); 92 | this.obs.send('SetSourceFilterVisibility', { 93 | sourceName: 'DSLR Face Cam GS For Mask', 94 | filterName: 'Sharp', 95 | filterEnabled: true, 96 | }); 97 | setTimeout(() => { 98 | this.obs.send('SetSceneItemProperties', { 99 | 'scene-name': 'DSLR Face Cam Composite', 100 | item: { name: `behind-you-${videoId}` }, 101 | visible: false, 102 | position: {}, 103 | bounds: {}, 104 | scale: {}, 105 | crop: {}, 106 | }); 107 | }, 300); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.html -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { BringFiniteWaterComponent } from './bring-finite-water.component'; 4 | 5 | describe('BringFiniteWaterComponent', () => { 6 | let component: BringFiniteWaterComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ BringFiniteWaterComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(BringFiniteWaterComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/bring-finite-water/bring-finite-water.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | import OBSWebSocket from 'obs-websocket-js'; 3 | import { Subscription } from 'rxjs'; 4 | import { 5 | ObsWebsocketsService, 6 | stopAdvKeys, 7 | startAdvKeys, 8 | } from 'src/app/services/obs-websockets.service'; 9 | import { MersenneTwister } from 'fast-mersenne-twister'; 10 | 11 | @Component({ 12 | selector: 'app-bring-finite-water', 13 | templateUrl: './bring-finite-water.component.html', 14 | styleUrls: ['./bring-finite-water.component.scss'], 15 | }) 16 | export class BringFiniteWaterComponent implements OnInit { 17 | private subs = new Subscription(); 18 | @Output() redemptionComplete = new EventEmitter(); 19 | @Input() obs: OBSWebSocket; 20 | lastEffect = 0; 21 | step = ''; 22 | constructor() {} 23 | 24 | ngOnInit(): void { 25 | this.bringFiniteWater(); 26 | } 27 | 28 | bringFiniteWater() { 29 | const rng = MersenneTwister(Date.now()); 30 | const effect = Math.ceil(rng.random() * 6); 31 | 32 | if (effect === 1) { 33 | this.finiteWater1(); 34 | } else if (effect === 2) { 35 | this.finiteWater2(); 36 | } else if (effect === 3) { 37 | this.finiteWater3(); 38 | } else if (effect === 4) { 39 | this.finiteWater4(); 40 | } else if (effect === 5) { 41 | this.finiteWater5(); 42 | } else if (effect === 6) { 43 | this.finiteWater6(); 44 | } 45 | } 46 | 47 | finiteWater1() { 48 | this.obs.send('SetSourceFilterSettings', { 49 | sourceName: 'bring-finite-water-1', 50 | filterName: 'Opacity', 51 | filterSettings: { 52 | opacity: 0, 53 | }, 54 | }); 55 | this.obs.send('SetSceneItemProperties', { 56 | 'scene-name': 'DSLR Face Cam Composite', 57 | item: { name: 'bring-finite-water-1' }, 58 | visible: true, 59 | position: {}, 60 | bounds: {}, 61 | scale: {}, 62 | crop: {}, 63 | }); 64 | this.obs.send('SetSourceFilterVisibility', { 65 | sourceName: 'bring-finite-water-1', 66 | filterName: 'FadeIn', 67 | filterEnabled: true, 68 | }); 69 | setTimeout(() => { 70 | this.obs.send('SetSourceFilterVisibility', { 71 | sourceName: 'Super Composite', 72 | filterName: 'BFW1-1', 73 | filterEnabled: true, 74 | }); 75 | this.obs.send('SetSourceFilterVisibility', { 76 | sourceName: 'DSLR Face Cam GS For Mask', 77 | filterName: 'BFW1-1', 78 | filterEnabled: true, 79 | }); 80 | }, 500); 81 | setTimeout(() => { 82 | this.obs.send('SetSceneItemProperties', { 83 | 'scene-name': 'DSLR Face Cam Composite', 84 | item: { name: 'bring-finite-water-1' }, 85 | visible: false, 86 | position: {}, 87 | bounds: {}, 88 | scale: {}, 89 | crop: {}, 90 | }); 91 | this.redemptionComplete.emit(); 92 | }, 23000); 93 | } 94 | 95 | finiteWater2() { 96 | this.obs.send('SetSourceFilterSettings', { 97 | sourceName: 'bring-finite-water-2', 98 | filterName: 'Opacity', 99 | filterSettings: { 100 | opacity: 0, 101 | }, 102 | }); 103 | this.obs.send('SetSceneItemProperties', { 104 | 'scene-name': 'DSLR Face Cam Composite', 105 | item: { name: 'bring-finite-water-2' }, 106 | visible: true, 107 | position: {}, 108 | bounds: {}, 109 | scale: {}, 110 | crop: {}, 111 | }); 112 | this.obs.send('SetSourceFilterVisibility', { 113 | sourceName: 'bring-finite-water-2', 114 | filterName: 'FadeIn', 115 | filterEnabled: true, 116 | }); 117 | setTimeout(() => { 118 | this.obs.send('SetSourceFilterVisibility', { 119 | sourceName: 'Super Composite', 120 | filterName: 'BFW2-1', 121 | filterEnabled: true, 122 | }); 123 | this.obs.send('SetSourceFilterVisibility', { 124 | sourceName: 'DSLR Face Cam GS For Mask', 125 | filterName: 'BFW2-1', 126 | filterEnabled: true, 127 | }); 128 | }, 5); 129 | setTimeout(() => { 130 | this.obs.send('SetSceneItemProperties', { 131 | 'scene-name': 'DSLR Face Cam Composite', 132 | item: { name: 'bring-finite-water-2' }, 133 | visible: false, 134 | position: {}, 135 | bounds: {}, 136 | scale: {}, 137 | crop: {}, 138 | }); 139 | this.redemptionComplete.emit(); 140 | }, 23000); 141 | } 142 | 143 | finiteWater3() { 144 | this.obs.send('SetSourceFilterSettings', { 145 | sourceName: 'bring-finite-water-3', 146 | filterName: 'Opacity', 147 | filterSettings: { 148 | opacity: 0, 149 | }, 150 | }); 151 | this.obs.send('SetSceneItemProperties', { 152 | 'scene-name': 'DSLR Face Cam Composite', 153 | item: { name: 'bring-finite-water-3' }, 154 | visible: true, 155 | position: {}, 156 | bounds: {}, 157 | scale: {}, 158 | crop: {}, 159 | }); 160 | this.obs.send('SetSourceFilterVisibility', { 161 | sourceName: 'bring-finite-water-3', 162 | filterName: 'FadeIn', 163 | filterEnabled: true, 164 | }); 165 | setTimeout(() => { 166 | this.obs.send('SetSourceFilterVisibility', { 167 | sourceName: 'Super Composite', 168 | filterName: 'BFW3-1', 169 | filterEnabled: true, 170 | }); 171 | this.obs.send('SetSourceFilterVisibility', { 172 | sourceName: 'DSLR Face Cam GS For Mask', 173 | filterName: 'BFW3-1', 174 | filterEnabled: true, 175 | }); 176 | }, 5); 177 | setTimeout(() => { 178 | this.obs.send('SetSceneItemProperties', { 179 | 'scene-name': 'DSLR Face Cam Composite', 180 | item: { name: 'bring-finite-water-3' }, 181 | visible: false, 182 | position: {}, 183 | bounds: {}, 184 | scale: {}, 185 | crop: {}, 186 | }); 187 | this.redemptionComplete.emit(); 188 | }, 36000); 189 | } 190 | 191 | finiteWater4() { 192 | this.obs.send('SetSourceFilterSettings', { 193 | sourceName: 'bring-finite-water-4', 194 | filterName: 'Opacity', 195 | filterSettings: { 196 | opacity: 0, 197 | }, 198 | }); 199 | this.obs.send('SetSceneItemProperties', { 200 | 'scene-name': 'DSLR Face Cam Composite', 201 | item: { name: 'bring-finite-water-4' }, 202 | visible: true, 203 | position: {}, 204 | bounds: {}, 205 | scale: {}, 206 | crop: {}, 207 | }); 208 | this.obs.send('SetSourceFilterVisibility', { 209 | sourceName: 'bring-finite-water-4', 210 | filterName: 'FadeIn', 211 | filterEnabled: true, 212 | }); 213 | setTimeout(() => { 214 | this.obs.send('SetSourceFilterVisibility', { 215 | sourceName: 'Super Composite', 216 | filterName: 'BFW4-1', 217 | filterEnabled: true, 218 | }); 219 | this.obs.send('SetSourceFilterVisibility', { 220 | sourceName: 'DSLR Face Cam GS For Mask', 221 | filterName: 'BFW4-1', 222 | filterEnabled: true, 223 | }); 224 | }, 5); 225 | setTimeout(() => { 226 | this.obs.send('SetSceneItemProperties', { 227 | 'scene-name': 'DSLR Face Cam Composite', 228 | item: { name: 'bring-finite-water-4' }, 229 | visible: false, 230 | position: {}, 231 | bounds: {}, 232 | scale: {}, 233 | crop: {}, 234 | }); 235 | this.redemptionComplete.emit(); 236 | }, 36000); 237 | } 238 | 239 | finiteWater5() { 240 | this.obs.send('SetSourceFilterSettings', { 241 | sourceName: 'bring-finite-water-5', 242 | filterName: 'Opacity', 243 | filterSettings: { 244 | opacity: 0, 245 | }, 246 | }); 247 | this.obs.send('SetSceneItemProperties', { 248 | 'scene-name': 'DSLR Face Cam Composite', 249 | item: { name: 'bring-finite-water-5' }, 250 | visible: true, 251 | position: {}, 252 | bounds: {}, 253 | scale: {}, 254 | crop: {}, 255 | }); 256 | this.obs.send('SetSourceFilterVisibility', { 257 | sourceName: 'bring-finite-water-5', 258 | filterName: 'FadeIn', 259 | filterEnabled: true, 260 | }); 261 | setTimeout(() => { 262 | this.obs.send('SetSourceFilterVisibility', { 263 | sourceName: 'Super Composite', 264 | filterName: 'BFW5-1', 265 | filterEnabled: true, 266 | }); 267 | this.obs.send('SetSourceFilterVisibility', { 268 | sourceName: 'DSLR Face Cam GS For Mask', 269 | filterName: 'BFW5-1', 270 | filterEnabled: true, 271 | }); 272 | }, 5); 273 | setTimeout(() => { 274 | this.obs.send('SetSceneItemProperties', { 275 | 'scene-name': 'DSLR Face Cam Composite', 276 | item: { name: 'bring-finite-water-5' }, 277 | visible: false, 278 | position: {}, 279 | bounds: {}, 280 | scale: {}, 281 | crop: {}, 282 | }); 283 | this.redemptionComplete.emit(); 284 | }, 11000); 285 | } 286 | 287 | finiteWater6() { 288 | this.obs.send('SetSourceFilterSettings', { 289 | sourceName: 'bring-finite-water-6', 290 | filterName: 'Opacity', 291 | filterSettings: { 292 | opacity: 0, 293 | }, 294 | }); 295 | this.obs.send('SetSceneItemProperties', { 296 | 'scene-name': 'DSLR Face Cam Composite', 297 | item: { name: 'bring-finite-water-6' }, 298 | visible: true, 299 | position: {}, 300 | bounds: {}, 301 | scale: {}, 302 | crop: {}, 303 | }); 304 | this.obs.send('SetSourceFilterVisibility', { 305 | sourceName: 'bring-finite-water-6', 306 | filterName: 'FadeIn', 307 | filterEnabled: true, 308 | }); 309 | setTimeout(() => { 310 | this.obs.send('SetSourceFilterVisibility', { 311 | sourceName: 'Super Composite', 312 | filterName: 'BFW6-1', 313 | filterEnabled: true, 314 | }); 315 | this.obs.send('SetSourceFilterVisibility', { 316 | sourceName: 'DSLR Face Cam GS For Mask', 317 | filterName: 'BFW6-1', 318 | filterEnabled: true, 319 | }); 320 | }, 5); 321 | setTimeout(() => { 322 | this.obs.send('SetSceneItemProperties', { 323 | 'scene-name': 'DSLR Face Cam Composite', 324 | item: { name: 'bring-finite-water-6' }, 325 | visible: false, 326 | position: {}, 327 | bounds: {}, 328 | scale: {}, 329 | crop: {}, 330 | }); 331 | this.redemptionComplete.emit(); 332 | }, 43000); 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.html -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CaptJackReviewComponent } from './capt-jack-review.component'; 4 | 5 | describe('CaptJackReviewComponent', () => { 6 | let component: CaptJackReviewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CaptJackReviewComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CaptJackReviewComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/capt-jack-review/capt-jack-review.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core'; 2 | import ObsWebSocket from 'obs-websocket-js'; 3 | import { MersenneTwister } from 'fast-mersenne-twister'; 4 | 5 | interface Background { 6 | video: string; 7 | audio: string; 8 | } 9 | 10 | interface ReviewEvent { 11 | event: string; 12 | time: number; 13 | duration?: number; 14 | } 15 | 16 | interface Review { 17 | video: string; 18 | events: ReviewEvent[]; 19 | } 20 | 21 | const BACKGROUNDS: Background[] = [ 22 | { video: 'capt-jack-bg-1', audio: 'capt-jack-bg-1-audio' }, 23 | { video: 'capt-jack-bg-2', audio: 'capt-jack-bg-2-audio' }, 24 | { video: 'capt-jack-bg-3', audio: 'capt-jack-bg-3-audio' }, 25 | ]; 26 | 27 | const REVIEWS: Review[] = [ 28 | { 29 | video: 'capt-jack-review-1', 30 | events: [ 31 | { event: 'door-open', time: 1100, duration: 1000 }, 32 | { event: 'door-close', time: 10350, duration: 1000 }, 33 | { event: 'unblur-facecam', time: 12833, duration: 1667 }, 34 | { event: 'blur-facecam', time: 22300, duration: 2500 }, 35 | { event: 'door-open', time: 26333, duration: 1000 }, 36 | { event: 'door-close', time: 30600, duration: 1000 }, 37 | { event: 'end', time: 32000 }, 38 | ], 39 | }, 40 | { 41 | video: 'capt-jack-review-2', 42 | events: [ 43 | { event: 'door-open', time: 1100, duration: 1000 }, 44 | { event: 'door-close', time: 4333, duration: 1000 }, 45 | { event: 'unblur-facecam', time: 5667, duration: 2333 }, 46 | { event: 'blur-facecam', time: 18667, duration: 3000 }, 47 | { event: 'door-open', time: 21667, duration: 1000 }, 48 | { event: 'door-close', time: 27400, duration: 1000 }, 49 | { event: 'end', time: 28000 }, 50 | ], 51 | }, 52 | { 53 | video: 'capt-jack-review-3', 54 | events: [ 55 | { event: 'door-open', time: 1100, duration: 1000 }, 56 | { event: 'door-close', time: 10200, duration: 1000 }, 57 | { event: 'unblur-facecam', time: 11667, duration: 2000 }, 58 | { event: 'blur-facecam', time: 21250, duration: 1250 }, 59 | { event: 'end', time: 28000 }, 60 | ], 61 | }, 62 | { 63 | video: 'capt-jack-review-4', 64 | events: [ 65 | { event: 'door-open', time: 1100, duration: 1000 }, 66 | { event: 'door-close', time: 6750, duration: 1000 }, 67 | { event: 'unblur-facecam', time: 8500, duration: 2450 }, 68 | { event: 'blur-facecam', time: 18750, duration: 2750 }, 69 | { event: 'door-open', time: 21250, duration: 1000 }, 70 | { event: 'door-close', time: 26200, duration: 1000 }, 71 | { event: 'end', time: 27500 }, 72 | ], 73 | }, 74 | { 75 | video: 'capt-jack-review-5', 76 | events: [ 77 | { event: 'door-open', time: 1100, duration: 1000 }, 78 | { event: 'door-close', time: 5900, duration: 1000 }, 79 | { event: 'unblur-facecam', time: 7000, duration: 1750 }, 80 | { event: 'blur-facecam', time: 15333, duration: 3333 }, 81 | { event: 'door-open', time: 18333, duration: 1000 }, 82 | { event: 'door-close', time: 23500, duration: 1000 }, 83 | { event: 'end', time: 24333 }, 84 | ], 85 | }, 86 | { 87 | video: 'capt-jack-review-6', 88 | events: [ 89 | { event: 'door-open', time: 1100, duration: 1000 }, 90 | { event: 'door-close', time: 5333, duration: 1000 }, 91 | { event: 'unblur-facecam', time: 6000, duration: 3000 }, 92 | { event: 'blur-facecam', time: 14750, duration: 2750 }, 93 | { event: 'door-open', time: 17500, duration: 1000 }, 94 | { event: 'door-close', time: 22333, duration: 1000 }, 95 | { event: 'end', time: 23200 }, 96 | ], 97 | }, 98 | { 99 | video: 'capt-jack-review-7', 100 | events: [ 101 | { event: 'door-open', time: 1100, duration: 1000 }, 102 | { event: 'door-close', time: 6000, duration: 1000 }, 103 | { event: 'unblur-facecam', time: 7250, duration: 2750 }, 104 | { event: 'blur-facecam', time: 18250, duration: 3250 }, 105 | { event: 'door-open', time: 21500, duration: 1000 }, 106 | { event: 'door-close', time: 26000, duration: 1000 }, 107 | { event: 'end', time: 27000 }, 108 | ], 109 | }, 110 | { 111 | video: 'capt-jack-review-8', 112 | events: [ 113 | { event: 'door-open', time: 1100, duration: 1000 }, 114 | { event: 'door-close', time: 6250, duration: 1000 }, 115 | { event: 'unblur-facecam', time: 7250, duration: 2750 }, 116 | { event: 'blur-facecam', time: 19750, duration: 2250 }, 117 | { event: 'door-open', time: 22000, duration: 1000 }, 118 | { event: 'door-close', time: 26833, duration: 1000 }, 119 | { event: 'end', time: 27500 }, 120 | ], 121 | }, 122 | { 123 | video: 'capt-jack-review-9', 124 | events: [ 125 | { event: 'door-open', time: 1100, duration: 1000 }, 126 | { event: 'door-close', time: 5500, duration: 1000 }, 127 | { event: 'unblur-facecam', time: 7000, duration: 1500 }, 128 | { event: 'blur-facecam', time: 18750, duration: 2750 }, 129 | { event: 'door-open', time: 21333, duration: 1000 }, 130 | { event: 'door-close', time: 27250, duration: 1000 }, 131 | { event: 'end', time: 28250 }, 132 | ], 133 | }, 134 | { 135 | video: 'capt-jack-review-10', 136 | events: [ 137 | { event: 'door-open', time: 1100, duration: 1000 }, 138 | { event: 'door-close', time: 4666, duration: 1000 }, 139 | { event: 'unblur-facecam', time: 5500, duration: 2000 }, 140 | { event: 'blur-facecam', time: 15000, duration: 2750 }, 141 | { event: 'door-open', time: 17750, duration: 1000 }, 142 | { event: 'door-close', time: 21666, duration: 1000 }, 143 | { event: 'end', time: 23000 }, 144 | ], 145 | }, 146 | ]; 147 | 148 | @Component({ 149 | selector: 'app-capt-jack-review', 150 | templateUrl: './capt-jack-review.component.html', 151 | styleUrls: ['./capt-jack-review.component.scss'], 152 | }) 153 | export class CaptJackReviewComponent implements OnInit { 154 | @Output() redemptionComplete = new EventEmitter(); 155 | @Output() updateHistory = new EventEmitter(); 156 | @Input() obs: ObsWebSocket; 157 | @Input() history: number[] = []; 158 | 159 | background: Background = null; 160 | review: Review = null; 161 | state: any; 162 | 163 | constructor() {} 164 | 165 | ngOnInit(): void { 166 | const rng = MersenneTwister(Date.now()); 167 | const bg = Math.floor(rng.random() * 3); 168 | let review = Math.floor(rng.random() * 10); 169 | while (this.history.includes(review)) { 170 | review = Math.floor(rng.random() * 10); 171 | } 172 | this.history.push(review); 173 | if (this.history.length > 5) { 174 | this.history = [...this.history.slice(-5)]; 175 | } 176 | this.updateHistory.emit(this.history); 177 | this.showCodeReview(bg, review); 178 | } 179 | 180 | async showCodeReview(bg = 0, review = 0) { 181 | this.background = BACKGROUNDS[bg]; 182 | this.review = REVIEWS[review]; 183 | await this.clearScene(); 184 | await this.setupScene(); 185 | this.fadeInScene(); 186 | 187 | for (const event of this.review.events) { 188 | setTimeout(() => { 189 | this.dispatchEvent(event); 190 | }, event.time); 191 | } 192 | } 193 | 194 | dispatchEvent(event: ReviewEvent) { 195 | console.log(event.event); 196 | switch (event.event) { 197 | case 'door-open': 198 | this.doorOpen(event); 199 | return; 200 | case 'door-close': 201 | this.doorClose(event); 202 | return; 203 | case 'unblur-facecam': 204 | this.unblur(event); 205 | return; 206 | case 'blur-facecam': 207 | this.blur(event); 208 | return; 209 | case 'end': 210 | this.end(event); 211 | return; 212 | } 213 | } 214 | 215 | doorOpen(event: ReviewEvent) { 216 | this.obs.send('SetSourceFilterVisibility', { 217 | sourceName: this.background.audio, 218 | filterName: 'fadeIn', 219 | filterEnabled: true, 220 | }); 221 | } 222 | 223 | doorClose(event: ReviewEvent) { 224 | this.obs.send('SetSourceFilterVisibility', { 225 | sourceName: this.background.audio, 226 | filterName: 'fadeOut', 227 | filterEnabled: true, 228 | }); 229 | } 230 | 231 | unblur(event: ReviewEvent) { 232 | this.sharpenFaceCam(event.duration); 233 | } 234 | 235 | blur(event: ReviewEvent) { 236 | this.blurFaceCam(event.duration); 237 | } 238 | 239 | end(event: ReviewEvent) { 240 | this.fadeOutScene(); 241 | setTimeout(async () => { 242 | await this.clearScene(); 243 | this.redemptionComplete.emit(); 244 | }, 1500); 245 | } 246 | 247 | async clearScene() { 248 | this.obs.send('SetSourceFilterSettings', { 249 | sourceName: 'Captain Jack', 250 | filterName: 'Opacity', 251 | filterSettings: { 252 | opacity: 0, 253 | }, 254 | }); 255 | for (const bg of BACKGROUNDS) { 256 | await this.obs.send('SetSceneItemProperties', { 257 | 'scene-name': 'Captain Jack Background', 258 | item: { name: bg.video }, 259 | visible: false, 260 | position: {}, 261 | bounds: {}, 262 | scale: {}, 263 | crop: {}, 264 | }); 265 | await this.obs.send('SetSceneItemProperties', { 266 | 'scene-name': 'Captain Jack Background', 267 | item: { name: bg.audio }, 268 | visible: false, 269 | position: {}, 270 | bounds: {}, 271 | scale: {}, 272 | crop: {}, 273 | }); 274 | } 275 | for (const review of REVIEWS) { 276 | await this.obs.send('SetSceneItemProperties', { 277 | 'scene-name': 'Captain Jack Greenscreen', 278 | item: { name: review.video }, 279 | visible: false, 280 | position: {}, 281 | bounds: {}, 282 | scale: {}, 283 | crop: {}, 284 | }); 285 | } 286 | const data = await this.obs.send('GetSourceFilterInfo', { 287 | sourceName: 'Super Composite', 288 | filterName: 'TimedSharpen', 289 | }); 290 | console.log(data); 291 | } 292 | 293 | async setupScene() { 294 | await this.obs.send('SetSceneItemProperties', { 295 | 'scene-name': 'Captain Jack Greenscreen', 296 | item: { name: this.review.video }, 297 | visible: true, 298 | position: {}, 299 | bounds: {}, 300 | scale: {}, 301 | crop: {}, 302 | }); 303 | 304 | await this.obs.send('SetSceneItemProperties', { 305 | 'scene-name': 'Captain Jack Background', 306 | item: { name: this.background.video }, 307 | visible: true, 308 | position: {}, 309 | bounds: {}, 310 | scale: {}, 311 | crop: {}, 312 | }); 313 | 314 | await this.obs.send('SetSceneItemProperties', { 315 | 'scene-name': 'Captain Jack Background', 316 | item: { name: this.background.audio }, 317 | visible: true, 318 | position: {}, 319 | bounds: {}, 320 | scale: {}, 321 | crop: {}, 322 | }); 323 | } 324 | 325 | async fadeInScene() { 326 | this.obs.send('SetSourceFilterVisibility', { 327 | sourceName: 'Captain Jack', 328 | filterName: 'FadeIn', 329 | filterEnabled: true, 330 | }); 331 | this.blurFaceCam(500); 332 | } 333 | 334 | async fadeOutScene() { 335 | this.obs.send('SetSourceFilterVisibility', { 336 | sourceName: 'Captain Jack', 337 | filterName: 'FadeOut', 338 | filterEnabled: true, 339 | }); 340 | this.sharpenFaceCam(500); 341 | } 342 | 343 | async blurFaceCam(duration: number) { 344 | await this.timedBlurFacecam('Blur', duration); 345 | } 346 | 347 | async sharpenFaceCam(duration: number) { 348 | await this.timedBlurFacecam('Sharpen', duration); 349 | } 350 | 351 | async timedBlurFacecam(operation: string, duration: number) { 352 | await this.obs.send('SetSourceFilterSettings', { 353 | sourceName: 'Super Composite', 354 | filterName: `Timed${operation}`, 355 | filterSettings: { 356 | duration, 357 | }, 358 | }); 359 | await this.obs.send('SetSourceFilterSettings', { 360 | sourceName: 'DSLR Face Cam GS For Mask', 361 | filterName: `Timed${operation}`, 362 | filterSettings: { 363 | duration, 364 | }, 365 | }); 366 | this.obs.send('SetSourceFilterVisibility', { 367 | sourceName: 'Super Composite', 368 | filterName: `Timed${operation}`, 369 | filterEnabled: true, 370 | }); 371 | this.obs.send('SetSourceFilterVisibility', { 372 | sourceName: 'DSLR Face Cam GS For Mask', 373 | filterName: `Timed${operation}`, 374 | filterEnabled: true, 375 | }); 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-cheer/chalkboard-cheer.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-cheer/chalkboard-cheer.component.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | margin: 0 auto; 3 | width: 300px; 4 | height: 300px; 5 | mask: url(/assets/brush-mask-1.png); 6 | 7 | .image-holder { 8 | width: 0px; 9 | height: 0px; 10 | overflow: hidden; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-cheer/chalkboard-cheer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardCheerComponent } from './chalkboard-cheer.component'; 4 | 5 | describe('ChalkboardCheerComponent', () => { 6 | let component: ChalkboardCheerComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardCheerComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardCheerComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-cheer/chalkboard-cheer.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | ViewChild 9 | } from '@angular/core'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | } from '@angular/animations'; 18 | 19 | import * as Vara from 'vara'; 20 | 21 | @Component({ 22 | selector: 'app-chalkboard-cheer', 23 | templateUrl: './chalkboard-cheer.component.html', 24 | styleUrls: ['./chalkboard-cheer.component.scss'], 25 | animations: [ 26 | trigger('openClose', [ 27 | state('open', style({ 28 | width: '300px', 29 | height: '300px', 30 | })), 31 | state('closed', style({ 32 | width: '0px', 33 | height: '0px', 34 | })), 35 | transition('open => closed', [ 36 | animate('1s') 37 | ]), 38 | transition('closed => open', [ 39 | animate('1s') 40 | ]), 41 | ]), 42 | ], 43 | }) 44 | export class ChalkboardCheerComponent implements OnInit { 45 | @Input() msg: any; 46 | @Input() extraData: any; 47 | 48 | @Output() writingComplete = new EventEmitter(); 49 | 50 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 51 | currentScene = ''; 52 | vara: any; 53 | showProfileImage = false; 54 | 55 | constructor() { } 56 | 57 | ngOnInit(): void { 58 | console.log('Cheer ngOnInit()'); 59 | console.log(this.extraData); 60 | this.setupMessage(); 61 | } 62 | 63 | setupMessage(): void { 64 | const data = this.msg.event_data; 65 | const message1 = `${data.user_name} cheered`; 66 | const message2 = `${data.bits} bits!` 67 | const message3 = `${data.message}`; 68 | this.writeText(message1, message2, message3); 69 | } 70 | 71 | writeText(message1: string, message2: string, message3: string) { 72 | const color = '#e9d5c8'; 73 | const ele = this.writingEle?.nativeElement; 74 | ele.innerHTML = "
"; 75 | this.vara = new Vara('#vara-container', 76 | "https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json", 77 | [ 78 | { 79 | text: message1, 80 | fromCurrentPosition: { y: true }, 81 | duration: 3000, 82 | color, 83 | x: 20, 84 | y: 20, 85 | }, 86 | { 87 | text: message2, 88 | fromCurrentPosition: { y: true }, 89 | duration: 1000, 90 | color, 91 | x: 5, 92 | y: 20, 93 | }, 94 | { 95 | text: message3, 96 | fromCurrentPosition: { y: true }, 97 | duration: 2000, 98 | color, 99 | x: 5, 100 | y: 20, 101 | } 102 | ], 103 | { 104 | fontSize: 64, 105 | strokeWidth: 2, 106 | textAlign: 'center', 107 | } 108 | ).animationEnd((i, o) => { 109 | if (i == 2) { 110 | this.showProfileImage = true; 111 | setTimeout(() => { 112 | this.eraseBoard(); 113 | this.showProfileImage = false; 114 | }, 3000); 115 | setTimeout(() => { 116 | ele.innerHTML = ""; 117 | this.writingComplete.emit(); 118 | }, 4500); 119 | } 120 | }); 121 | } 122 | 123 | eraseBoard() { 124 | // TODO: Make this simpler! 125 | const varaContainer = document.getElementById('vara-container'); 126 | if (varaContainer) { 127 | const svg = varaContainer.getElementsByTagName('svg')[0]; 128 | const svgWidth = svg.getBoundingClientRect().width; 129 | const svgHeight = svg.getBoundingClientRect().height; 130 | const paths = Array.from(svg.children); 131 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 132 | paths.forEach((p: any) => { 133 | const pBbox = p.getBoundingClientRect(); 134 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 135 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 136 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 137 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 138 | }); 139 | const mask = document.createElementNS("http://www.w3.org/2000/svg", "mask"); 140 | const maskFill = document.createElementNS("http://www.w3.org/2000/svg", "rect"); 141 | const path = document.createElementNS("http://www.w3.org/2000/svg", "path"); 142 | const g = document.createElementNS("http://www.w3.org/2000/svg", "g"); 143 | 144 | mask.setAttribute("id", "mask"); 145 | 146 | maskFill.setAttribute("x", "0"); 147 | maskFill.setAttribute("y", "0"); 148 | maskFill.setAttribute("width", `${svgWidth}`); 149 | maskFill.setAttribute("height", `${svgHeight}`); 150 | maskFill.setAttribute("fill", "white"); 151 | 152 | const numLegs = 10; 153 | const start = { x: bbox.xMin, y: bbox.yMin }; 154 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 155 | 156 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 157 | for (let step = 0; step < numLegs; step++) { 158 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 159 | const x = bbox.xMin + (step + 1) * dx; 160 | d += ` L${x},${y}`; 161 | } 162 | path.setAttribute("d", d); 163 | path.setAttribute("class", "erase"); 164 | const length = path.getTotalLength(); 165 | path.style.strokeDasharray = length + ' ' + length; 166 | path.style.strokeDashoffset = `${length}`; 167 | 168 | mask.appendChild(maskFill); 169 | mask.appendChild(path); 170 | svg.appendChild(mask); 171 | g.setAttribute("mask", "url(#mask)"); 172 | svg.appendChild(g); 173 | paths.filter(p => p.tagName === 'g').forEach(p => g.appendChild(p)); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-gift-sub/chalkboard-gift-sub.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-gift-sub/chalkboard-gift-sub.component.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | margin: 0 auto; 3 | width: 300px; 4 | height: 300px; 5 | mask: url(/assets/brush-mask-1.png); 6 | 7 | .image-holder { 8 | width: 0px; 9 | height: 0px; 10 | overflow: hidden; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-gift-sub/chalkboard-gift-sub.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardGiftSubComponent } from './chalkboard-gift-sub.component'; 4 | 5 | describe('ChalkboardGiftSubComponent', () => { 6 | let component: ChalkboardGiftSubComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardGiftSubComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardGiftSubComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-gift-sub/chalkboard-gift-sub.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | ViewChild, 9 | } from '@angular/core'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | } from '@angular/animations'; 18 | 19 | import * as Vara from 'vara'; 20 | 21 | @Component({ 22 | selector: 'app-chalkboard-gift-sub', 23 | templateUrl: './chalkboard-gift-sub.component.html', 24 | styleUrls: ['./chalkboard-gift-sub.component.scss'], 25 | animations: [ 26 | trigger('openClose', [ 27 | state( 28 | 'open', 29 | style({ 30 | width: '300px', 31 | height: '300px', 32 | }) 33 | ), 34 | state( 35 | 'closed', 36 | style({ 37 | width: '0px', 38 | height: '0px', 39 | }) 40 | ), 41 | transition('open => closed', [animate('1s')]), 42 | transition('closed => open', [animate('1s')]), 43 | ]), 44 | ], 45 | }) 46 | export class ChalkboardGiftSubComponent implements OnInit { 47 | @Input() msg: any; 48 | @Input() extraData: any; 49 | 50 | @Output() writingComplete = new EventEmitter(); 51 | 52 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 53 | currentScene = ''; 54 | vara: any; 55 | showProfileImage = false; 56 | 57 | constructor() {} 58 | 59 | ngOnInit(): void { 60 | this.setupMessage(); 61 | } 62 | 63 | setupMessage(): void { 64 | console.log('setup message'); 65 | const data = this.msg.event_data; 66 | const gifts = data.total; 67 | const tier = 68 | data.tier === '1000' 69 | ? 'Tier 1' 70 | : data.tier === '2000' 71 | ? 'Tier 2' 72 | : data.tier === '3000' 73 | ? 'Tier 3' 74 | : 'prime'; 75 | const cumulative_total = data.cumulative_total; 76 | 77 | const message1 = `${data.user_name} gifted ${gifts}`; 78 | const message2 = `${tier} subs.`; 79 | const message3 = `They have gifted ${cumulative_total} total subs.`; 80 | this.writeText(message1, message2, message3); 81 | } 82 | 83 | writeText(message1: string, message2: string, message3: string) { 84 | const color = '#e9d5c8'; 85 | const ele = this.writingEle?.nativeElement; 86 | ele.innerHTML = "
"; 87 | this.vara = new Vara( 88 | '#vara-container', 89 | 'https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json', 90 | [ 91 | { 92 | text: message1, 93 | fromCurrentPosition: { y: true }, 94 | duration: 3000, 95 | color, 96 | x: 0, 97 | y: 8, 98 | }, 99 | { 100 | text: message2, 101 | fromCurrentPosition: { y: true }, 102 | duration: 1000, 103 | color, 104 | x: 0, 105 | y: 8, 106 | fontSize: 64, 107 | }, 108 | { 109 | text: message3, 110 | fromCurrentPosition: { y: true }, 111 | duration: 2000, 112 | color, 113 | x: 0, 114 | y: 8, 115 | }, 116 | ], 117 | { 118 | fontSize: 64, 119 | strokeWidth: 2, 120 | textAlign: 'center', 121 | } 122 | ).animationEnd((i, o) => { 123 | if (i == 2) { 124 | this.showProfileImage = true; 125 | setTimeout(() => { 126 | this.showProfileImage = false; 127 | this.eraseBoard(); 128 | }, 3000); 129 | setTimeout(() => { 130 | ele.innerHTML = ''; 131 | this.writingComplete.emit(); 132 | }, 4500); 133 | } 134 | }); 135 | } 136 | 137 | eraseBoard() { 138 | // TODO: Make this simpler! 139 | const varaContainer = document.getElementById('vara-container'); 140 | if (varaContainer) { 141 | const svg = varaContainer.getElementsByTagName('svg')[0]; 142 | const svgWidth = svg.getBoundingClientRect().width; 143 | const svgHeight = svg.getBoundingClientRect().height; 144 | const paths = Array.from(svg.children); 145 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 146 | paths.forEach((p: any) => { 147 | const pBbox = p.getBoundingClientRect(); 148 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 149 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 150 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 151 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 152 | }); 153 | const mask = document.createElementNS( 154 | 'http://www.w3.org/2000/svg', 155 | 'mask' 156 | ); 157 | const maskFill = document.createElementNS( 158 | 'http://www.w3.org/2000/svg', 159 | 'rect' 160 | ); 161 | const path = document.createElementNS( 162 | 'http://www.w3.org/2000/svg', 163 | 'path' 164 | ); 165 | const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 166 | 167 | mask.setAttribute('id', 'mask'); 168 | 169 | maskFill.setAttribute('x', '0'); 170 | maskFill.setAttribute('y', '0'); 171 | maskFill.setAttribute('width', `${svgWidth}`); 172 | maskFill.setAttribute('height', `${svgHeight}`); 173 | maskFill.setAttribute('fill', 'white'); 174 | 175 | const numLegs = 10; 176 | const start = { x: bbox.xMin, y: bbox.yMin }; 177 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 178 | 179 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 180 | for (let step = 0; step < numLegs; step++) { 181 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 182 | const x = bbox.xMin + (step + 1) * dx; 183 | d += ` L${x},${y}`; 184 | } 185 | path.setAttribute('d', d); 186 | path.setAttribute('class', 'erase'); 187 | const length = path.getTotalLength(); 188 | path.style.strokeDasharray = length + ' ' + length; 189 | path.style.strokeDashoffset = `${length}`; 190 | 191 | mask.appendChild(maskFill); 192 | mask.appendChild(path); 193 | svg.appendChild(mask); 194 | g.setAttribute('mask', 'url(#mask)'); 195 | svg.appendChild(g); 196 | paths.filter((p) => p.tagName === 'g').forEach((p) => g.appendChild(p)); 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-point-test/chalkboard-point-test.component.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-point-test/chalkboard-point-test.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/chalkboard-point-test/chalkboard-point-test.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-point-test/chalkboard-point-test.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardPointTestComponent } from './chalkboard-point-test.component'; 4 | 5 | describe('ChalkboardPointTestComponent', () => { 6 | let component: ChalkboardPointTestComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardPointTestComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardPointTestComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-point-test/chalkboard-point-test.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | Input, 5 | OnInit, 6 | Output, 7 | ViewChild, 8 | EventEmitter, 9 | } from '@angular/core'; 10 | import * as Vara from 'vara'; 11 | import { isString } from 'lodash'; 12 | import OBSWebSocket from 'obs-websocket-js'; 13 | import { MersenneTwister } from 'fast-mersenne-twister'; 14 | 15 | const timingsMap = { 16 | 1: { 17 | startWriting: 8000, 18 | eraseTimeout: 10500, 19 | removeChalkTimeout: 14500, 20 | turnOffVidTimeout: 13500, 21 | endTimeout: 13500, 22 | }, 23 | 2: { 24 | startWriting: 8000, 25 | eraseTimeout: 10500, 26 | removeChalkTimeout: 14500, 27 | turnOffVidTimeout: 13500, 28 | endTimeout: 13500, 29 | }, 30 | 3: { 31 | startWriting: 4000, 32 | eraseTimeout: 10500, 33 | removeChalkTimeout: 14500, 34 | turnOffVidTimeout: 11500, 35 | endTimeout: 11500, 36 | }, 37 | 4: { 38 | startWriting: 6000, 39 | eraseTimeout: 10500, 40 | removeChalkTimeout: 14500, 41 | turnOffVidTimeout: 13500, 42 | endTimeout: 13500, 43 | }, 44 | 5: { 45 | startWriting: 2000, 46 | eraseTimeout: 9500, 47 | removeChalkTimeout: 10500, 48 | turnOffVidTimeout: 12500, 49 | endTimeout: 12500, 50 | }, 51 | 6: { 52 | startWriting: 2500, 53 | eraseTimeout: 10500, 54 | removeChalkTimeout: 12500, 55 | turnOffVidTimeout: 14500, 56 | endTimeout: 14500, 57 | }, 58 | 7: { 59 | startWriting: 4000, 60 | eraseTimeout: 10500, 61 | removeChalkTimeout: 17500, 62 | turnOffVidTimeout: 20500, 63 | endTimeout: 20500, 64 | }, 65 | 8: { 66 | startWriting: 4000, 67 | eraseTimeout: 10500, 68 | removeChalkTimeout: 14500, 69 | turnOffVidTimeout: 17500, 70 | endTimeout: 17500, 71 | }, 72 | }; 73 | 74 | @Component({ 75 | selector: 'app-chalkboard-point-test', 76 | templateUrl: './chalkboard-point-test.component.html', 77 | styleUrls: ['./chalkboard-point-test.component.scss'], 78 | }) 79 | export class ChalkboardPointTestComponent implements OnInit { 80 | @Input() msg: any; 81 | @Input() obs: OBSWebSocket; 82 | 83 | @Output() writingComplete = new EventEmitter(); 84 | 85 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 86 | currentScene = ''; 87 | vara: any; 88 | 89 | constructor() {} 90 | 91 | ngOnInit(): void { 92 | console.log('Cheer ngOnInit()'); 93 | this.setupMessage(); 94 | } 95 | 96 | setupMessage(): void { 97 | const data = this.msg.event_data; 98 | const message1 = `${data.user_name} says:`; 99 | const message2 = isString(data.user_input) 100 | ? data.user_input 101 | : data.user_input.message; 102 | this.writeText(message1, message2); 103 | } 104 | 105 | turnOnVideo(videoId: number) { 106 | this.obs.send('SetSourceFilterSettings', { 107 | sourceName: `writing-on-wall-${videoId}`, 108 | filterName: 'Opacity', 109 | filterSettings: { 110 | opacity: 0, 111 | }, 112 | }); 113 | this.obs.send('SetSceneItemProperties', { 114 | 'scene-name': 'DSLR Face Cam Composite', 115 | item: { name: `writing-on-wall-${videoId}` }, 116 | visible: true, 117 | position: {}, 118 | bounds: {}, 119 | scale: {}, 120 | crop: {}, 121 | }); 122 | this.obs.send('SetSourceFilterVisibility', { 123 | sourceName: `writing-on-wall-${videoId}`, 124 | filterName: 'FadeIn', 125 | filterEnabled: true, 126 | }); 127 | this.obs.send('SetSourceFilterVisibility', { 128 | sourceName: 'Super Composite', 129 | filterName: 'Blurry', 130 | filterEnabled: true, 131 | }); 132 | this.obs.send('SetSourceFilterVisibility', { 133 | sourceName: 'DSLR Face Cam GS For Mask', 134 | filterName: 'Blurry', 135 | filterEnabled: true, 136 | }); 137 | } 138 | 139 | turnOffVideo(videoId: number) { 140 | this.obs.send('SetSourceFilterVisibility', { 141 | sourceName: `writing-on-wall-${videoId}`, 142 | filterName: 'FadeOut', 143 | filterEnabled: true, 144 | }); 145 | this.obs.send('SetSourceFilterVisibility', { 146 | sourceName: 'Super Composite', 147 | filterName: 'Sharp', 148 | filterEnabled: true, 149 | }); 150 | this.obs.send('SetSourceFilterVisibility', { 151 | sourceName: 'DSLR Face Cam GS For Mask', 152 | filterName: 'Sharp', 153 | filterEnabled: true, 154 | }); 155 | setTimeout(() => { 156 | this.obs.send('SetSceneItemProperties', { 157 | 'scene-name': 'DSLR Face Cam Composite', 158 | item: { name: `writing-on-wall-${videoId}` }, 159 | visible: false, 160 | position: {}, 161 | bounds: {}, 162 | scale: {}, 163 | crop: {}, 164 | }); 165 | }, 300); 166 | } 167 | 168 | writeText(message1: string, message2: string) { 169 | const rng = MersenneTwister(Date.now()); 170 | const effect = Math.ceil(rng.random() * 8); 171 | const timings = timingsMap[effect]; 172 | this.turnOnVideo(effect); 173 | 174 | const color = '#e9d5c8'; 175 | const ele = this.writingEle?.nativeElement; 176 | ele.innerHTML = "
"; 177 | setTimeout(() => { 178 | this.vara = new Vara( 179 | '#vara-container', 180 | 'https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json', 181 | [ 182 | { 183 | text: message1, 184 | fromCurrentPosition: { y: true }, 185 | duration: 3000, 186 | color, 187 | x: 20, 188 | y: 20, 189 | }, 190 | { 191 | text: message2, 192 | fromCurrentPosition: { y: true }, 193 | duration: 1000, 194 | color, 195 | x: 5, 196 | y: 20, 197 | }, 198 | ], 199 | { 200 | fontSize: 64, 201 | strokeWidth: 2, 202 | textAlign: 'center', 203 | } 204 | ).animationEnd((i, o) => { 205 | if (i == 1) { 206 | setTimeout(() => { 207 | this.eraseBoard(); 208 | }, timings.eraseTimeout); 209 | setTimeout(() => { 210 | const ele = this.writingEle?.nativeElement; 211 | ele.innerHTML = ''; 212 | }, timings.removeChalkTimeout); 213 | setTimeout(() => { 214 | this.turnOffVideo(effect); 215 | }, timings.turnOffVidTimeout); 216 | setTimeout(() => { 217 | ele.innerHTML = ''; 218 | this.writingComplete.emit(); 219 | }, timings.endTimeout); 220 | } 221 | }); 222 | }, timings.startWriting); 223 | } 224 | 225 | eraseBoard() { 226 | // TODO: Make this simpler! 227 | const varaContainer = document.getElementById('vara-container'); 228 | if (varaContainer) { 229 | const svg = varaContainer.getElementsByTagName('svg')[0]; 230 | const svgWidth = svg.getBoundingClientRect().width; 231 | const svgHeight = svg.getBoundingClientRect().height; 232 | const paths = Array.from(svg.children); 233 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 234 | paths.forEach((p: any) => { 235 | const pBbox = p.getBoundingClientRect(); 236 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 237 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 238 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 239 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 240 | }); 241 | const mask = document.createElementNS( 242 | 'http://www.w3.org/2000/svg', 243 | 'mask' 244 | ); 245 | const maskFill = document.createElementNS( 246 | 'http://www.w3.org/2000/svg', 247 | 'rect' 248 | ); 249 | const path = document.createElementNS( 250 | 'http://www.w3.org/2000/svg', 251 | 'path' 252 | ); 253 | const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 254 | 255 | mask.setAttribute('id', 'mask'); 256 | 257 | maskFill.setAttribute('x', '0'); 258 | maskFill.setAttribute('y', '0'); 259 | maskFill.setAttribute('width', `${svgWidth}`); 260 | maskFill.setAttribute('height', `${svgHeight}`); 261 | maskFill.setAttribute('fill', 'white'); 262 | 263 | const numLegs = 10; 264 | const start = { x: bbox.xMin, y: bbox.yMin }; 265 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 266 | 267 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 268 | for (let step = 0; step < numLegs; step++) { 269 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 270 | const x = bbox.xMin + (step + 1) * dx; 271 | d += ` L${x},${y}`; 272 | } 273 | path.setAttribute('d', d); 274 | path.setAttribute('class', 'erase'); 275 | const length = path.getTotalLength(); 276 | path.style.strokeDasharray = length + ' ' + length; 277 | path.style.strokeDashoffset = `${length}`; 278 | 279 | mask.appendChild(maskFill); 280 | mask.appendChild(path); 281 | svg.appendChild(mask); 282 | g.setAttribute('mask', 'url(#mask)'); 283 | svg.appendChild(g); 284 | paths.filter((p) => p.tagName === 'g').forEach((p) => g.appendChild(p)); 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-raid/chalkboard-raid.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-raid/chalkboard-raid.component.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | margin: 0 auto; 3 | width: 300px; 4 | height: 300px; 5 | mask: url(/assets/brush-mask-1.png); 6 | 7 | .image-holder { 8 | width: 0px; 9 | height: 0px; 10 | overflow: hidden; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-raid/chalkboard-raid.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardRaidComponent } from './chalkboard-raid.component'; 4 | 5 | describe('ChalkboardRaidComponent', () => { 6 | let component: ChalkboardRaidComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardRaidComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardRaidComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-raid/chalkboard-raid.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | ViewChild, 9 | } from '@angular/core'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | } from '@angular/animations'; 18 | 19 | import * as Vara from 'vara'; 20 | import OBSWebSocket from 'obs-websocket-js'; 21 | import { MersenneTwister } from 'fast-mersenne-twister'; 22 | 23 | const timingsMap = { 24 | 1: { 25 | eraseTimeout: 6500, 26 | removeChalkTimeout: 8000, 27 | turnOffVidTimeout: 9500, 28 | endTimeout: 10000, 29 | }, 30 | 2: { 31 | eraseTimeout: 4500, 32 | removeChalkTimeout: 6500, 33 | turnOffVidTimeout: 7500, 34 | endTimeout: 8000, 35 | }, 36 | }; 37 | 38 | @Component({ 39 | selector: 'app-chalkboard-raid', 40 | templateUrl: './chalkboard-raid.component.html', 41 | styleUrls: ['./chalkboard-raid.component.scss'], 42 | animations: [ 43 | trigger('openClose', [ 44 | state( 45 | 'open', 46 | style({ 47 | width: '300px', 48 | height: '300px', 49 | }) 50 | ), 51 | state( 52 | 'closed', 53 | style({ 54 | width: '0px', 55 | height: '0px', 56 | }) 57 | ), 58 | transition('open => closed', [animate('1s')]), 59 | transition('closed => open', [animate('1s')]), 60 | ]), 61 | ], 62 | }) 63 | export class ChalkboardRaidComponent implements OnInit { 64 | @Input() msg: any; 65 | @Input() extraData: any; 66 | @Input() obs: OBSWebSocket; 67 | 68 | @Output() writingComplete = new EventEmitter(); 69 | 70 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 71 | currentScene = ''; 72 | vara: any; 73 | showProfileImage = false; 74 | 75 | constructor() {} 76 | 77 | ngOnInit(): void { 78 | console.log('Raid ngOnInit()'); 79 | this.setupMessage(); 80 | } 81 | 82 | setupMessage(): void { 83 | const data = this.msg.event_data; 84 | const message1 = `${data.from_broadcaster_user_name} raided`; 85 | const message2 = `with ${data.viewers} viewers!`; 86 | this.writeText(message1, message2); 87 | } 88 | 89 | turnOnVideo(videoId: number) { 90 | this.obs.send('SetSourceFilterSettings', { 91 | sourceName: `raid-${videoId}`, 92 | filterName: 'Opacity', 93 | filterSettings: { 94 | opacity: 0, 95 | }, 96 | }); 97 | this.obs.send('SetSceneItemProperties', { 98 | 'scene-name': 'DSLR Face Cam Composite', 99 | item: { name: `raid-${videoId}` }, 100 | visible: true, 101 | position: {}, 102 | bounds: {}, 103 | scale: {}, 104 | crop: {}, 105 | }); 106 | this.obs.send('SetSourceFilterVisibility', { 107 | sourceName: `raid-${videoId}`, 108 | filterName: 'FadeIn', 109 | filterEnabled: true, 110 | }); 111 | this.obs.send('SetSourceFilterVisibility', { 112 | sourceName: 'Super Composite', 113 | filterName: 'Blurry', 114 | filterEnabled: true, 115 | }); 116 | this.obs.send('SetSourceFilterVisibility', { 117 | sourceName: 'DSLR Face Cam GS For Mask', 118 | filterName: 'Blurry', 119 | filterEnabled: true, 120 | }); 121 | } 122 | 123 | turnOffVideo(videoId: number) { 124 | this.obs.send('SetSourceFilterVisibility', { 125 | sourceName: `raid-${videoId}`, 126 | filterName: 'FadeOut', 127 | filterEnabled: true, 128 | }); 129 | this.obs.send('SetSourceFilterVisibility', { 130 | sourceName: 'Super Composite', 131 | filterName: 'Sharp', 132 | filterEnabled: true, 133 | }); 134 | this.obs.send('SetSourceFilterVisibility', { 135 | sourceName: 'DSLR Face Cam GS For Mask', 136 | filterName: 'Sharp', 137 | filterEnabled: true, 138 | }); 139 | setTimeout(() => { 140 | this.obs.send('SetSceneItemProperties', { 141 | 'scene-name': 'DSLR Face Cam Composite', 142 | item: { name: `raid-${videoId}` }, 143 | visible: false, 144 | position: {}, 145 | bounds: {}, 146 | scale: {}, 147 | crop: {}, 148 | }); 149 | }, 300); 150 | } 151 | 152 | writeText(message1: string, message2: string) { 153 | const rng = MersenneTwister(Date.now()); 154 | const effect = Math.ceil(rng.random() * 2); 155 | 156 | const timings = timingsMap[effect]; 157 | // Turn on raid video 158 | this.turnOnVideo(effect); 159 | 160 | const color = '#e9d5c8'; 161 | const ele = this.writingEle?.nativeElement; 162 | ele.innerHTML = "
"; 163 | this.vara = new Vara( 164 | '#vara-container', 165 | 'https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json', 166 | [ 167 | { 168 | text: message1, 169 | fromCurrentPosition: { y: true }, 170 | duration: 3000, 171 | color, 172 | x: 0, 173 | y: 20, 174 | }, 175 | { 176 | text: message2, 177 | fromCurrentPosition: { y: true }, 178 | duration: 1000, 179 | color, 180 | x: 0, 181 | y: 20, 182 | }, 183 | ], 184 | { 185 | fontSize: 64, 186 | strokeWidth: 2, 187 | textAlign: 'center', 188 | } 189 | ).animationEnd((i, o) => { 190 | if (i == 1) { 191 | this.showProfileImage = true; 192 | setTimeout(() => { 193 | this.eraseBoard(); 194 | this.showProfileImage = false; 195 | }, timings.eraseTimeout); 196 | setTimeout(() => { 197 | const ele = this.writingEle?.nativeElement; 198 | ele.innerHTML = ''; 199 | }, timings.removeChalkTimeout); 200 | setTimeout(() => { 201 | this.turnOffVideo(effect); 202 | }, timings.turnOffVidTimeout); 203 | setTimeout(() => { 204 | ele.innerHTML = ''; 205 | this.writingComplete.emit(); 206 | }, timings.endTimeout); 207 | } 208 | }); 209 | } 210 | 211 | eraseBoard() { 212 | // TODO: Make this simpler! 213 | const varaContainer = document.getElementById('vara-container'); 214 | if (varaContainer) { 215 | const svg = varaContainer.getElementsByTagName('svg')[0]; 216 | const svgWidth = svg.getBoundingClientRect().width; 217 | const svgHeight = svg.getBoundingClientRect().height; 218 | const paths = Array.from(svg.children); 219 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 220 | paths.forEach((p: any) => { 221 | const pBbox = p.getBoundingClientRect(); 222 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 223 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 224 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 225 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 226 | }); 227 | const mask = document.createElementNS( 228 | 'http://www.w3.org/2000/svg', 229 | 'mask' 230 | ); 231 | const maskFill = document.createElementNS( 232 | 'http://www.w3.org/2000/svg', 233 | 'rect' 234 | ); 235 | const path = document.createElementNS( 236 | 'http://www.w3.org/2000/svg', 237 | 'path' 238 | ); 239 | const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 240 | 241 | mask.setAttribute('id', 'mask'); 242 | 243 | maskFill.setAttribute('x', '0'); 244 | maskFill.setAttribute('y', '0'); 245 | maskFill.setAttribute('width', `${svgWidth}`); 246 | maskFill.setAttribute('height', `${svgHeight}`); 247 | maskFill.setAttribute('fill', 'white'); 248 | 249 | const numLegs = 10; 250 | const start = { x: bbox.xMin, y: bbox.yMin }; 251 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 252 | 253 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 254 | for (let step = 0; step < numLegs; step++) { 255 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 256 | const x = bbox.xMin + (step + 1) * dx; 257 | d += ` L${x},${y}`; 258 | } 259 | path.setAttribute('d', d); 260 | path.setAttribute('class', 'erase'); 261 | const length = path.getTotalLength(); 262 | path.style.strokeDasharray = length + ' ' + length; 263 | path.style.strokeDashoffset = `${length}`; 264 | 265 | mask.appendChild(maskFill); 266 | mask.appendChild(path); 267 | svg.appendChild(mask); 268 | g.setAttribute('mask', 'url(#mask)'); 269 | svg.appendChild(g); 270 | paths.filter((p) => p.tagName === 'g').forEach((p) => g.appendChild(p)); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-sub-no-message/chalkboard-sub-no-message.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-sub-no-message/chalkboard-sub-no-message.component.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | margin: 0 auto; 3 | width: 300px; 4 | height: 300px; 5 | mask: url(/assets/brush-mask-1.png); 6 | 7 | .image-holder { 8 | width: 0px; 9 | height: 0px; 10 | overflow: hidden; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-sub-no-message/chalkboard-sub-no-message.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardSubNoMessageComponent } from './chalkboard-sub-no-message.component'; 4 | 5 | describe('ChalkboardSubNoMessageComponent', () => { 6 | let component: ChalkboardSubNoMessageComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardSubNoMessageComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardSubNoMessageComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-sub-no-message/chalkboard-sub-no-message.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | ViewChild, 9 | } from '@angular/core'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | } from '@angular/animations'; 18 | 19 | import * as Vara from 'vara'; 20 | 21 | @Component({ 22 | selector: 'app-chalkboard-sub-no-message', 23 | templateUrl: './chalkboard-sub-no-message.component.html', 24 | styleUrls: ['./chalkboard-sub-no-message.component.scss'], 25 | animations: [ 26 | trigger('openClose', [ 27 | state( 28 | 'open', 29 | style({ 30 | width: '300px', 31 | height: '300px', 32 | }) 33 | ), 34 | state( 35 | 'closed', 36 | style({ 37 | width: '0px', 38 | height: '0px', 39 | }) 40 | ), 41 | transition('open => closed', [animate('1s')]), 42 | transition('closed => open', [animate('1s')]), 43 | ]), 44 | ], 45 | }) 46 | export class ChalkboardSubNoMessageComponent implements OnInit { 47 | @Input() msg: any; 48 | @Input() extraData: any; 49 | 50 | @Output() writingComplete = new EventEmitter(); 51 | 52 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 53 | currentScene = ''; 54 | vara: any; 55 | showProfileImage = false; 56 | 57 | constructor() {} 58 | 59 | ngOnInit(): void { 60 | this.setupMessage(); 61 | } 62 | 63 | setupMessage(): void { 64 | const data = this.msg.event_data; 65 | const tier = 66 | data.tier === '1000' 67 | ? 'at Tier 1' 68 | : data.tier === '2000' 69 | ? 'at Tier 2' 70 | : data.tier === '3000' 71 | ? 'at Tier 3' 72 | : 'with prime'; 73 | 74 | const duration = 75 | data.cumulative_months === '1' 76 | ? '' 77 | : `for ${data.cumulative_months} months`; 78 | 79 | const message1 = `${data.user_name} Subscribed`; 80 | const message2 = `${tier}`; 81 | this.writeText(message1, message2); 82 | } 83 | 84 | writeText(message1: string, message2: string) { 85 | const color = '#e9d5c8'; 86 | const ele = this.writingEle?.nativeElement; 87 | ele.innerHTML = "
"; 88 | this.vara = new Vara( 89 | '#vara-container', 90 | 'https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json', 91 | [ 92 | { 93 | text: message1, 94 | fromCurrentPosition: { y: true }, 95 | duration: 3000, 96 | color, 97 | x: 0, 98 | y: 8, 99 | }, 100 | { 101 | text: message2, 102 | fromCurrentPosition: { y: true }, 103 | duration: 1000, 104 | color, 105 | x: 0, 106 | y: 8, 107 | fontSize: 64, 108 | }, 109 | ], 110 | { 111 | fontSize: 64, 112 | strokeWidth: 2, 113 | textAlign: 'center', 114 | } 115 | ).animationEnd((i, o) => { 116 | if (i == 1) { 117 | this.showProfileImage = true; 118 | setTimeout(() => { 119 | this.showProfileImage = false; 120 | this.eraseBoard(); 121 | }, 3000); 122 | setTimeout(() => { 123 | ele.innerHTML = ''; 124 | this.writingComplete.emit(); 125 | }, 4500); 126 | } 127 | }); 128 | } 129 | 130 | eraseBoard() { 131 | // TODO: Make this simpler! 132 | const varaContainer = document.getElementById('vara-container'); 133 | if (varaContainer) { 134 | const svg = varaContainer.getElementsByTagName('svg')[0]; 135 | const svgWidth = svg.getBoundingClientRect().width; 136 | const svgHeight = svg.getBoundingClientRect().height; 137 | const paths = Array.from(svg.children); 138 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 139 | paths.forEach((p: any) => { 140 | const pBbox = p.getBoundingClientRect(); 141 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 142 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 143 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 144 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 145 | }); 146 | const mask = document.createElementNS( 147 | 'http://www.w3.org/2000/svg', 148 | 'mask' 149 | ); 150 | const maskFill = document.createElementNS( 151 | 'http://www.w3.org/2000/svg', 152 | 'rect' 153 | ); 154 | const path = document.createElementNS( 155 | 'http://www.w3.org/2000/svg', 156 | 'path' 157 | ); 158 | const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 159 | 160 | mask.setAttribute('id', 'mask'); 161 | 162 | maskFill.setAttribute('x', '0'); 163 | maskFill.setAttribute('y', '0'); 164 | maskFill.setAttribute('width', `${svgWidth}`); 165 | maskFill.setAttribute('height', `${svgHeight}`); 166 | maskFill.setAttribute('fill', 'white'); 167 | 168 | const numLegs = 10; 169 | const start = { x: bbox.xMin, y: bbox.yMin }; 170 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 171 | 172 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 173 | for (let step = 0; step < numLegs; step++) { 174 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 175 | const x = bbox.xMin + (step + 1) * dx; 176 | d += ` L${x},${y}`; 177 | } 178 | path.setAttribute('d', d); 179 | path.setAttribute('class', 'erase'); 180 | const length = path.getTotalLength(); 181 | path.style.strokeDasharray = length + ' ' + length; 182 | path.style.strokeDashoffset = `${length}`; 183 | 184 | mask.appendChild(maskFill); 185 | mask.appendChild(path); 186 | svg.appendChild(mask); 187 | g.setAttribute('mask', 'url(#mask)'); 188 | svg.appendChild(g); 189 | paths.filter((p) => p.tagName === 'g').forEach((p) => g.appendChild(p)); 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-subscribe/chalkboard-subscribe.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-subscribe/chalkboard-subscribe.component.scss: -------------------------------------------------------------------------------- 1 | .profile-picture { 2 | margin: 0 auto; 3 | width: 300px; 4 | height: 300px; 5 | mask: url(/assets/brush-mask-1.png); 6 | 7 | .image-holder { 8 | width: 0px; 9 | height: 0px; 10 | overflow: hidden; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-subscribe/chalkboard-subscribe.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ChalkboardSubscribeComponent } from './chalkboard-subscribe.component'; 4 | 5 | describe('ChalkboardSubscribeComponent', () => { 6 | let component: ChalkboardSubscribeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ChalkboardSubscribeComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ChalkboardSubscribeComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/chalkboard-subscribe/chalkboard-subscribe.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | EventEmitter, 5 | Input, 6 | OnInit, 7 | Output, 8 | ViewChild, 9 | } from '@angular/core'; 10 | 11 | import { 12 | trigger, 13 | state, 14 | style, 15 | animate, 16 | transition, 17 | } from '@angular/animations'; 18 | 19 | import * as Vara from 'vara'; 20 | 21 | @Component({ 22 | selector: 'app-chalkboard-subscribe', 23 | templateUrl: './chalkboard-subscribe.component.html', 24 | styleUrls: ['./chalkboard-subscribe.component.scss'], 25 | animations: [ 26 | trigger('openClose', [ 27 | state( 28 | 'open', 29 | style({ 30 | width: '300px', 31 | height: '300px', 32 | }) 33 | ), 34 | state( 35 | 'closed', 36 | style({ 37 | width: '0px', 38 | height: '0px', 39 | }) 40 | ), 41 | transition('open => closed', [animate('1s')]), 42 | transition('closed => open', [animate('1s')]), 43 | ]), 44 | ], 45 | }) 46 | export class ChalkboardSubscribeComponent implements OnInit { 47 | @Input() msg: any; 48 | @Input() extraData: any; 49 | 50 | @Output() writingComplete = new EventEmitter(); 51 | 52 | @ViewChild('writingContainer', { static: true }) writingEle?: ElementRef; 53 | currentScene = ''; 54 | vara: any; 55 | showProfileImage = false; 56 | 57 | constructor() {} 58 | 59 | ngOnInit(): void { 60 | this.setupMessage(); 61 | } 62 | 63 | setupMessage(): void { 64 | const data = this.msg.event_data; 65 | const tier = 66 | data.tier === '1000' 67 | ? 'at Tier 1' 68 | : data.tier === '2000' 69 | ? 'at Tier 2' 70 | : data.tier === '3000' 71 | ? 'at Tier 3' 72 | : 'with prime'; 73 | 74 | const duration = 75 | data.cumulative_months === '1' 76 | ? '' 77 | : `for ${data.cumulative_months} months`; 78 | 79 | const message1 = `${data.user_name} Subscribed`; 80 | const message2 = `${tier} ${duration}`; 81 | const message3 = data.message.text; 82 | this.writeText(message1, message2, message3); 83 | } 84 | 85 | writeText(message1: string, message2: string, message3: string) { 86 | const color = '#e9d5c8'; 87 | const ele = this.writingEle?.nativeElement; 88 | ele.innerHTML = "
"; 89 | this.vara = new Vara( 90 | '#vara-container', 91 | 'https://rawcdn.githack.com/akzhy/Vara/ed6ab92fdf196596266ae76867c415fa659eb348/fonts/Satisfy/SatisfySL.json', 92 | [ 93 | { 94 | text: message1, 95 | fromCurrentPosition: { y: true }, 96 | duration: 3000, 97 | color, 98 | x: 0, 99 | y: 8, 100 | }, 101 | { 102 | text: message2, 103 | fromCurrentPosition: { y: true }, 104 | duration: 1000, 105 | color, 106 | x: 0, 107 | y: 8, 108 | fontSize: 64, 109 | }, 110 | { 111 | text: message3, 112 | fromCurrentPosition: { y: true }, 113 | duration: 2000, 114 | color, 115 | x: 0, 116 | y: 8, 117 | }, 118 | ], 119 | { 120 | fontSize: 64, 121 | strokeWidth: 2, 122 | textAlign: 'center', 123 | } 124 | ).animationEnd((i, o) => { 125 | if (i == 2) { 126 | this.showProfileImage = true; 127 | setTimeout(() => { 128 | this.showProfileImage = false; 129 | this.eraseBoard(); 130 | }, 3000); 131 | setTimeout(() => { 132 | ele.innerHTML = ''; 133 | this.writingComplete.emit(); 134 | }, 4500); 135 | } 136 | }); 137 | } 138 | 139 | eraseBoard() { 140 | // TODO: Make this simpler! 141 | const varaContainer = document.getElementById('vara-container'); 142 | if (varaContainer) { 143 | const svg = varaContainer.getElementsByTagName('svg')[0]; 144 | const svgWidth = svg.getBoundingClientRect().width; 145 | const svgHeight = svg.getBoundingClientRect().height; 146 | const paths = Array.from(svg.children); 147 | const bbox = { xMin: 999, xMax: -999, yMin: 999, yMax: -999 }; 148 | paths.forEach((p: any) => { 149 | const pBbox = p.getBoundingClientRect(); 150 | bbox.xMin = Math.min(bbox.xMin, pBbox.x); 151 | bbox.yMin = Math.min(bbox.yMin, pBbox.y); 152 | bbox.xMax = Math.max(bbox.xMax, pBbox.x + pBbox.width); 153 | bbox.yMax = Math.max(bbox.yMax, pBbox.y + pBbox.height); 154 | }); 155 | const mask = document.createElementNS( 156 | 'http://www.w3.org/2000/svg', 157 | 'mask' 158 | ); 159 | const maskFill = document.createElementNS( 160 | 'http://www.w3.org/2000/svg', 161 | 'rect' 162 | ); 163 | const path = document.createElementNS( 164 | 'http://www.w3.org/2000/svg', 165 | 'path' 166 | ); 167 | const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); 168 | 169 | mask.setAttribute('id', 'mask'); 170 | 171 | maskFill.setAttribute('x', '0'); 172 | maskFill.setAttribute('y', '0'); 173 | maskFill.setAttribute('width', `${svgWidth}`); 174 | maskFill.setAttribute('height', `${svgHeight}`); 175 | maskFill.setAttribute('fill', 'white'); 176 | 177 | const numLegs = 10; 178 | const start = { x: bbox.xMin, y: bbox.yMin }; 179 | const dx = (bbox.xMax - bbox.xMin) / (numLegs + 1); 180 | 181 | let d = `M${bbox.xMin}, ${bbox.yMin}`; 182 | for (let step = 0; step < numLegs; step++) { 183 | const y = step % 2 == 0 ? bbox.yMax : bbox.yMin; 184 | const x = bbox.xMin + (step + 1) * dx; 185 | d += ` L${x},${y}`; 186 | } 187 | path.setAttribute('d', d); 188 | path.setAttribute('class', 'erase'); 189 | const length = path.getTotalLength(); 190 | path.style.strokeDasharray = length + ' ' + length; 191 | path.style.strokeDashoffset = `${length}`; 192 | 193 | mask.appendChild(maskFill); 194 | mask.appendChild(path); 195 | svg.appendChild(mask); 196 | g.setAttribute('mask', 'url(#mask)'); 197 | svg.appendChild(g); 198 | paths.filter((p) => p.tagName === 'g').forEach((p) => g.appendChild(p)); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/message-matrix/message-matrix.component.html: -------------------------------------------------------------------------------- 1 |

message-matrix works!

2 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/message-matrix/message-matrix.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/chalkboard-overlay/components/message-matrix/message-matrix.component.scss -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/message-matrix/message-matrix.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { MessageMatrixComponent } from './message-matrix.component'; 4 | 5 | describe('MessageMatrixComponent', () => { 6 | let component: MessageMatrixComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ MessageMatrixComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MessageMatrixComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/components/message-matrix/message-matrix.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | import { EmoteMessage } from 'src/app/models/emote-message'; 4 | 5 | @Component({ 6 | selector: 'app-message-matrix', 7 | templateUrl: './message-matrix.component.html', 8 | styleUrls: ['./message-matrix.component.scss'] 9 | }) 10 | export class MessageMatrixComponent implements OnInit { 11 | @Input() message: EmoteMessage; 12 | 13 | constructor(private http: HttpClient) { } 14 | 15 | ngOnInit(): void { 16 | console.log(this.message); 17 | // this.http.get( 18 | // `http://192.168.1.115/message?message=${this.message}&red=255&green=128&blue=128` 19 | // ).subscribe((resp) => {console.log(resp);}); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/services/cheer-component.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { CheerComponentService } from './cheer-component.service'; 4 | 5 | describe('CheerComponentService', () => { 6 | let service: CheerComponentService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(CheerComponentService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/features/chalkboard-overlay/services/cheer-component.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class CheerComponentService { 7 | 8 | constructor() { } 9 | } 10 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | import { CirclesBinningComponent } from "./circles-binning.component"; 3 | 4 | export const CirclesBinningRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: CirclesBinningComponent 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning.component.scss: -------------------------------------------------------------------------------- 1 | #canvas { 2 | position: absolute; 3 | width: 1000px; 4 | height: 800px; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CirclesBinningComponent } from './circles-binning.component'; 4 | 5 | describe('CirclesBinningComponent', () => { 6 | let component: CirclesBinningComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CirclesBinningComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CirclesBinningComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit } from '@angular/core'; 2 | import * as paper from 'paper'; 3 | 4 | import { Circle } from '../../models/circle'; 5 | import { BoundingBox } from '../../models/bounding-box'; 6 | 7 | @Component({ 8 | selector: 'app-circles-binning', 9 | templateUrl: './circles-binning.component.html', 10 | styleUrls: ['./circles-binning.component.scss'] 11 | }) 12 | export class CirclesBinningComponent implements OnInit, AfterViewInit { 13 | primaryCircle: Circle = { 14 | center: { x: 500, y: 400 }, 15 | r: 200, 16 | color: [0, 0, 200] 17 | }; 18 | 19 | coverCircles: Circle[] = [ 20 | { 21 | center: { x: 600, y: 505 }, 22 | r: 150, 23 | color: [200, 0, 0] 24 | }, 25 | { 26 | center: { x: 800, y: 400 }, 27 | r: 125, 28 | color: [200, 0, 0] 29 | }, 30 | { 31 | center: { x: 700, y: 250 }, 32 | r: 150, 33 | color: [200, 0, 0] 34 | }, 35 | { 36 | center: { x: 300, y: 400 }, 37 | r: 300, 38 | color: [200, 0, 0] 39 | } 40 | ] 41 | 42 | constructor() { } 43 | 44 | ngOnInit(): void { 45 | } 46 | 47 | ngAfterViewInit(): void { 48 | console.log('AfterViewInit'); 49 | this.setupCanvas(); 50 | this.drawCircles(); 51 | 52 | this.coverCircles = this.coverCircles.sort((a, b) => { 53 | return a.center.y > b.center.y ? 1 : 1; 54 | }); 55 | 56 | const area = this.calculateArea(this.primaryCircle); 57 | const estimate = this.estimateArea(this.primaryCircle, 2); 58 | const error = (estimate - area) / area; 59 | console.log(area, estimate, error); 60 | const compoundArea = this.estimateCompoundArea(this.primaryCircle, this.coverCircles, 1); 61 | console.log(compoundArea); 62 | } 63 | 64 | setupCanvas() { 65 | const canvas = document.getElementById("canvas") as HTMLCanvasElement; 66 | paper.setup(canvas); 67 | } 68 | 69 | drawCircles() { 70 | const primary = this.drawCircle(this.primaryCircle); 71 | this.coverCircles.forEach(data => this.drawCircle(data)); 72 | this.drawBoundingBox(this.getBoundingBox(this.primaryCircle)); 73 | this.coverCircles.forEach(data => this.drawBoundingBox(this.getBoundingBox(data))); 74 | } 75 | 76 | drawBoundingBox(data: BoundingBox) { 77 | const bb = new paper.Path.Rectangle( 78 | new paper.Point(data.minBound.x, data.minBound.y), 79 | new paper.Point(data.maxBound.x, data.maxBound.y), 80 | ); 81 | bb.strokeColor = new paper.Color( 82 | 0, 0, 0, 1.0 83 | ); 84 | } 85 | 86 | drawCircle(data: Circle) { 87 | const circle = new paper.Path.Circle( 88 | new paper.Point(data.center.x, data.center.y), 89 | data.r 90 | ); 91 | circle.strokeColor = new paper.Color( 92 | ...data.color, 1.0 93 | ); 94 | circle.fillColor = new paper.Color( 95 | ...data.color, 0.3 96 | ) 97 | return circle; 98 | } 99 | 100 | getBoundingBox(circle: Circle): BoundingBox { 101 | return { 102 | minBound: { x: circle.center.x - circle.r, y: circle.center.y - circle.r }, 103 | maxBound: { x: circle.center.x + circle.r, y: circle.center.y + circle.r } 104 | }; 105 | } 106 | 107 | calculateArea(circle: Circle): number { 108 | return Math.PI * circle.r ** 2; 109 | } 110 | 111 | estimateArea(circle: Circle, binSize = 1): number { 112 | const bb = this.getBoundingBox(circle); 113 | let area = 0; 114 | for (let y = bb.minBound.y; y <= bb.maxBound.y; y += binSize) { 115 | const c = Math.sqrt(circle.r ** 2 - (y - circle.center.y) ** 2) 116 | const xMin = circle.center.x - c; 117 | const xMax = circle.center.x + c; 118 | area += (xMax - xMin) * binSize; 119 | } 120 | return area; 121 | } 122 | 123 | estimateCompoundArea(circle: Circle, overlapCircles: Circle[], binSize = 1): number { 124 | let area = 0 125 | const bb = this.getBoundingBox(circle); 126 | 127 | for (let y = bb.minBound.y; y <= bb.maxBound.y; y += binSize) { 128 | let checked = false; 129 | let cBounds = this.scanLineBounds(circle, y); 130 | let scanLineArea = (cBounds.xMax - cBounds.xMin) * binSize; 131 | 132 | const scanLineOverlap = overlapCircles.filter(c => { 133 | return (c.center.y - c.r <= y) && (c.center.y + c.r >= y) 134 | }); 135 | let overlapBounds: { xMin: number, xMax: number }[] = []; 136 | scanLineOverlap.forEach(slo => { 137 | const bounds = this.scanLineBounds(slo, y); 138 | if (bounds.xMin < cBounds.xMax && bounds.xMax > cBounds.xMin) { 139 | bounds.xMin = Math.max(bounds.xMin, cBounds.xMin); 140 | bounds.xMax = Math.min(bounds.xMax, cBounds.xMax); 141 | overlapBounds.push({ ...bounds }); 142 | } 143 | }); 144 | overlapBounds = overlapBounds.sort((a, b) => a.xMin < b.xMin ? -1 : 1); 145 | 146 | const constrainedBounds = []; 147 | overlapBounds.forEach((bounds) => { 148 | const length = constrainedBounds.length; 149 | if (length === 0) { 150 | constrainedBounds.push({ ...bounds }); 151 | } else if (bounds.xMin <= constrainedBounds[length - 1].xMax) { 152 | constrainedBounds[length - 1].xMax = bounds.xMax 153 | } else { 154 | constrainedBounds.push({ ...bounds }); 155 | } 156 | }); 157 | constrainedBounds.forEach(bounds => { 158 | scanLineArea -= (bounds.xMax - bounds.xMin); 159 | }) 160 | area += scanLineArea; 161 | } 162 | 163 | return area; 164 | } 165 | 166 | scanLineBounds(circle: Circle, y: number) { 167 | const c = Math.sqrt(circle.r ** 2 - (y - circle.center.y) ** 2) 168 | const xMin = circle.center.x - c; 169 | const xMax = circle.center.x + c; 170 | return { xMin, xMax }; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/app/features/circles-binning/circles-binning.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CirclesBinningRoutes } from './circles-binning-routing.module'; 4 | import { RouterModule } from '@angular/router'; 5 | 6 | 7 | 8 | @NgModule({ 9 | declarations: [], 10 | imports: [ 11 | CommonModule, 12 | RouterModule.forChild(CirclesBinningRoutes) 13 | ] 14 | }) 15 | export class CirclesBinningModule { } 16 | -------------------------------------------------------------------------------- /src/app/features/circles/circles-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | import { CirclesComponent } from "./circles.component"; 3 | 4 | export const CirclesRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: CirclesComponent 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/circles/circles.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/features/circles/circles.component.scss: -------------------------------------------------------------------------------- 1 | #canvas { 2 | position: absolute; 3 | width: 1000px; 4 | height: 800px; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/features/circles/circles.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CirclesComponent } from './circles.component'; 4 | 5 | describe('CirclesComponent', () => { 6 | let component: CirclesComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ CirclesComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CirclesComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/circles/circles.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, OnInit } from '@angular/core'; 2 | import * as paper from 'paper'; 3 | 4 | // Algorthm idea for common overlap from: 5 | // https://github.com/benfred/bens-blog-code/blob/master/circle-intersection/circle-intersection.js 6 | 7 | const SMALL = 1e-10; 8 | 9 | interface CircleData { 10 | x: number; 11 | y: number; 12 | r: number; 13 | color: [number, number, number]; 14 | } 15 | 16 | interface LineData { 17 | x1: number; 18 | y1: number; 19 | x2: number; 20 | y2: number; 21 | color: [number, number, number]; 22 | } 23 | 24 | interface Point { 25 | x: number; 26 | y: number; 27 | } 28 | 29 | interface IndexedPoint { 30 | point: Point; 31 | angle: number; 32 | parents: CircleData[]; 33 | } 34 | 35 | @Component({ 36 | selector: 'app-circles', 37 | templateUrl: './circles.component.html', 38 | styleUrls: ['./circles.component.scss'] 39 | }) 40 | export class CirclesComponent implements OnInit, AfterViewInit { 41 | primaryCircle: CircleData = { 42 | x: 500, 43 | y: 400, 44 | r: 200, 45 | color: [0, 0, 200] 46 | }; 47 | 48 | coverCircles: CircleData[] = [ 49 | { 50 | x: 600, 51 | y: 505, 52 | r: 150, 53 | color: [200, 0, 0] 54 | }, 55 | { 56 | x: 650, 57 | y: 400, 58 | r: 125, 59 | color: [200, 0, 0] 60 | }, 61 | { 62 | x: 700, 63 | y: 450, 64 | r: 150, 65 | color: [200, 0, 0] 66 | } 67 | ] 68 | 69 | constructor() { } 70 | 71 | ngOnInit(): void { 72 | 73 | } 74 | 75 | ngAfterViewInit(): void { 76 | this.setupCanvas(); 77 | this.drawCircles(); 78 | } 79 | 80 | setupCanvas() { 81 | const canvas = document.getElementById("canvas") as HTMLCanvasElement; 82 | paper.setup(canvas); 83 | // paper.project.view.viewSize = new paper.Size(600, 600); 84 | } 85 | 86 | calculateAllOverlap() { 87 | 88 | const c1Overlaps = this.coverCircles.map((c2) => { 89 | return this.overlapArea(this.primaryCircle, c2) 90 | }) 91 | } 92 | 93 | drawCircles() { 94 | const primary = this.drawCircle(this.primaryCircle); 95 | this.coverCircles.forEach(data => this.drawCircle(data)); 96 | 97 | this.calculateAllOverlap(); 98 | const area = this.intersectionArea([this.primaryCircle, ...this.coverCircles]); 99 | console.log(area); 100 | } 101 | 102 | drawCircle(data: CircleData) { 103 | const circle = new paper.Path.Circle( 104 | new paper.Point(data.x, data.y), 105 | data.r 106 | ); 107 | circle.strokeColor = new paper.Color( 108 | ...data.color, 1.0 109 | ); 110 | circle.fillColor = new paper.Color( 111 | ...data.color, 0.3 112 | ) 113 | return circle; 114 | } 115 | 116 | drawLine(data: LineData) { 117 | const line = new paper.Path.Line( 118 | new paper.Point(data.x1, data.y1), 119 | new paper.Point(data.x2, data.y2) 120 | ); 121 | line.strokeColor = new paper.Color( 122 | ...data.color, 1.0 123 | ); 124 | return line; 125 | } 126 | 127 | distance(c1: CircleData, c2: CircleData): number { 128 | return this.dist({ x: c1.x, y: c1.y }, { x: c2.x, y: c2.y }); 129 | } 130 | 131 | dist(p1: Point, p2: Point): number { 132 | return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2); 133 | } 134 | 135 | dist2(p1: Point, p2: Point): number { 136 | return (p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2; 137 | } 138 | 139 | overlapping(c1: CircleData, c2: CircleData): boolean { 140 | return this.distance(c1, c2) < c1.r + c2.r; 141 | } 142 | 143 | overlapArea(c1: CircleData, c2: CircleData): number { 144 | const c1c2 = this.dist2( 145 | { x: c1.x, y: c1.y }, { x: c2.x, y: c2.y } 146 | ); 147 | 148 | const c1c2d = this.dist( 149 | { x: c1.x, y: c1.y }, { x: c2.x, y: c2.y } 150 | ) 151 | 152 | if (c1c2d + c2.r <= (c1.r)) { 153 | return this.circleArea(c2); 154 | } 155 | 156 | const intersection = this.intersectionPoints(c1, c2); 157 | const cl = this.dist(intersection[0], intersection[1]); 158 | const intMid = { 159 | x: (intersection[0].x + intersection[1].x) / 2.0, 160 | y: (intersection[0].y + intersection[1].y) / 2.0, 161 | } 162 | 163 | 164 | 165 | const c1Mid = this.dist2( 166 | { x: c1.x, y: c1.y }, intMid 167 | ); 168 | 169 | const c2Mid = this.dist2( 170 | { x: c2.x, y: c2.y }, intMid 171 | ) 172 | 173 | const c1Segment = c1c2 > c2Mid; 174 | 175 | const c2Segment = c1c2 > c1Mid; 176 | 177 | const c1LensArea = c1Segment ? 178 | this.segmentArea(c1, cl) : 179 | this.circleArea(c1) - this.segmentArea(c1, cl); 180 | 181 | const c2LensArea = c2Segment ? 182 | this.segmentArea(c2, cl) : 183 | this.circleArea(c2) - this.segmentArea(c2, cl); 184 | 185 | return c1LensArea + c2LensArea; 186 | } 187 | 188 | circleArea(cir: CircleData): number { 189 | return Math.PI * cir.r ** 2; 190 | } 191 | 192 | segmentArea(cir: CircleData, cl: number): number { 193 | const alpha = 2.0 * Math.asin(cl / (2 * cir.r)); 194 | return 0.5 * cir.r ** 2 * (alpha - Math.sin(alpha)); 195 | } 196 | 197 | intersectionPoints(c1: CircleData, c2: CircleData): Point[] { 198 | const D = this.distance(c1, c2); 199 | 200 | if ((D >= (c1.r + c2.r)) || (D <= Math.abs(c1.r - c2.r))) { 201 | return []; 202 | } 203 | 204 | const delta = 0.25 * Math.sqrt( 205 | (D + c1.r + c2.r) * (D + c1.r - c2.r) * (D - c1.r + c2.r) * (-D + c1.r + c2.r) 206 | ); 207 | 208 | const Xc = (c1.x + c2.x) / 2 + (c2.x - c1.x) * 209 | (Math.pow(c1.r, 2) - Math.pow(c2.r, 2)) / (2 * D * D); 210 | 211 | const Yc = (c1.y + c2.y) / 2 + (c2.y - c1.y) * 212 | (Math.pow(c1.r, 2) - Math.pow(c2.r, 2)) / (2 * D * D); 213 | 214 | return [ 215 | { 216 | x: Xc + 2 * (c1.y - c2.y) / (D * D) * delta, 217 | y: Yc - 2 * (c1.x - c2.x) / (D * D) * delta, 218 | }, 219 | { 220 | x: Xc - 2 * (c1.y - c2.y) / (D * D) * delta, 221 | y: Yc + 2 * (c1.x - c2.x) / (D * D) * delta, 222 | }]; 223 | } 224 | 225 | intersectionArea(circles: CircleData[]) { 226 | const intersectionPoints = this.getIntersectionPoints(circles); 227 | intersectionPoints.forEach((point: IndexedPoint) => { 228 | this.drawCircle({ 229 | x: point.point.x, 230 | y: point.point.y, 231 | r: 3, 232 | color: [0, 0, 200] 233 | }); 234 | }) 235 | 236 | const innerPoints = intersectionPoints.filter(p => this.containedInCircles(p, circles)); 237 | 238 | innerPoints.forEach((point: IndexedPoint) => { 239 | this.drawCircle({ 240 | x: point.point.x, 241 | y: point.point.y, 242 | r: 3, 243 | color: [200, 0, 0] 244 | }); 245 | }) 246 | 247 | let arcArea = 0, polygonArea = 0, arcs = [], i: number; 248 | 249 | if (innerPoints.length > 1) { 250 | const center = this.getCenter(innerPoints); 251 | innerPoints.forEach(point => { 252 | point.angle = Math.atan2(point.point.x - center.x, point.point.y - center.y); 253 | }) 254 | 255 | innerPoints.sort(function (a, b) { return b.angle - a.angle; }); 256 | let p2 = innerPoints[innerPoints.length - 1]; 257 | innerPoints.forEach(p1 => { 258 | // polygon area updates easily ... 259 | polygonArea += (p2.point.x + p1.point.x) * (p1.point.y - p2.point.y); 260 | 261 | // updating the arc area is a little more involved 262 | const midPoint = { 263 | x: (p1.point.x + p2.point.x) / 2, 264 | y: (p1.point.y + p2.point.y) / 2 265 | }; 266 | let arc = null; 267 | 268 | p1.parents.forEach(p1p => { 269 | // if (p2.parentIndex.indexOf(p1.parentIndex[j]) > -1) { 270 | if (p2.parents.findIndex( 271 | p2p => p2p.x === p1p.x && p2p.y === p1p.y && p2p.r === p1p.r 272 | ) > -1 273 | ) { 274 | // figure out the angle halfway between the two points 275 | // on the current circle 276 | const a1 = Math.atan2(p1.point.x - p1p.x, p1.point.y - p1p.y); 277 | const a2 = Math.atan2(p2.point.x - p1p.x, p2.point.y - p1p.y); 278 | 279 | var angleDiff = (a2 - a1); 280 | if (angleDiff < 0) { 281 | angleDiff += 2 * Math.PI; 282 | } 283 | 284 | // and use that angle to figure out the width of the 285 | // arc 286 | var a = a2 - angleDiff / 2, 287 | width = this.dist(midPoint, { 288 | x: p1p.x + p1p.r * Math.sin(a), 289 | y: p1p.y + p1p.r * Math.cos(a) 290 | }); 291 | 292 | // pick the circle whose arc has the smallest width 293 | if ((arc === null) || (arc.width > width)) { 294 | arc = { 295 | circle: p1p, 296 | width: width, 297 | p1: p1, 298 | p2: p2 299 | }; 300 | } 301 | } 302 | }); 303 | arcs.push(arc); 304 | arcArea += this.circleAreaWidth(arc.circle.r, arc.width); 305 | p2 = p1; 306 | }); 307 | } else { 308 | // no intersection points, is either disjoint - or is completely 309 | // overlapped. figure out which by examining the smallest circle 310 | const smallest = circles.reduce((prev, curr) => { 311 | return prev.r < curr.r ? prev : curr; 312 | }); 313 | 314 | // make sure the smallest circle is completely contained in all 315 | // the other circles 316 | let disjoint = false; 317 | for (i = 0; i < circles.length; ++i) { 318 | const circle = circles[i]; 319 | const dist = this.dist({ x: circle.x, y: circle.y }, { x: smallest.x, y: smallest.y }); 320 | if (dist > Math.abs(smallest.r - circle.r)) { 321 | disjoint = true; 322 | break; 323 | } 324 | } 325 | 326 | if (disjoint) { 327 | arcArea = polygonArea = 0; 328 | 329 | } else { 330 | arcArea = smallest.r * smallest.r * Math.PI; 331 | arcs.push({ 332 | circle: smallest, 333 | p1: { x: smallest.x, y: smallest.y + smallest.r }, 334 | p2: { x: smallest.x - SMALL, y: smallest.y + smallest.r }, 335 | width: smallest.r * 2 336 | }); 337 | } 338 | } 339 | polygonArea /= 2; 340 | 341 | return arcArea + polygonArea; 342 | } 343 | 344 | getIntersectionPoints(circles: CircleData[]) { 345 | const ret: IndexedPoint[] = []; 346 | 347 | circles.forEach((c1, i) => { 348 | circles.slice(i + 1).forEach((c2, j) => { 349 | const intersect = this.intersectionPoints(c1, c2); 350 | intersect.forEach(point => { 351 | ret.push({ 352 | point, 353 | angle: 0, 354 | parents: [c1, c2] 355 | }); 356 | }); 357 | }); 358 | }); 359 | return ret; 360 | } 361 | 362 | containedInCircles(point: IndexedPoint, circles: CircleData[]) { 363 | 364 | for (let i = 0; i < circles.length; ++i) { 365 | if (this.dist2(point.point, circles[i]) > (circles[i].r + SMALL) ** 2) { 366 | return false; 367 | } 368 | } 369 | return true; 370 | } 371 | 372 | getCenter(points: IndexedPoint[]): Point { 373 | const center: Point = { x: 0, y: 0 }; 374 | points.forEach(point => { 375 | center.x += point.point.x, 376 | center.y += point.point.y 377 | }); 378 | center.x /= points.length; 379 | center.y /= points.length; 380 | return center; 381 | } 382 | 383 | circleIntegral(r: number, x: number): number { 384 | var y = Math.sqrt(r * r - x * x); 385 | return x * y + r * r * Math.atan2(x, y); 386 | }; 387 | 388 | /** Returns the area of a circle of radius r - up to width */ 389 | circleAreaWidth(r: number, width: number): number { 390 | return this.circleIntegral(r, width - r) - this.circleIntegral(r, -r); 391 | }; 392 | 393 | } 394 | -------------------------------------------------------------------------------- /src/app/features/circles/circles.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { CirclesComponent } from '../circles/circles.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { CirclesRoutes } from './circles-routing.module'; 6 | 7 | 8 | 9 | @NgModule({ 10 | declarations: [CirclesComponent], 11 | imports: [ 12 | CommonModule, 13 | RouterModule.forChild(CirclesRoutes) 14 | ] 15 | }) 16 | export class CirclesModule { } 17 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/components/follow/follow.component.html: -------------------------------------------------------------------------------- 1 |

{{ msg.event_data.user_name }} Followed!

2 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/components/follow/follow.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/general-overlay/components/follow/follow.component.scss -------------------------------------------------------------------------------- /src/app/features/general-overlay/components/follow/follow.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { FollowComponent } from './follow.component'; 4 | 5 | describe('FollowComponent', () => { 6 | let component: FollowComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ FollowComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(FollowComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/components/follow/follow.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-follow', 5 | templateUrl: './follow.component.html', 6 | styleUrls: ['./follow.component.scss'] 7 | }) 8 | export class FollowComponent implements OnInit { 9 | @Input() msg: any; 10 | @Output() displayComplete = new EventEmitter(); 11 | 12 | constructor() { } 13 | 14 | ngOnInit(): void { 15 | setTimeout(() => { 16 | this.displayComplete.emit(); 17 | }, 4000); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | import { GeneralOverlayComponent } from "./general-overlay.component"; 3 | 4 | export const GeneralOverlayRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: GeneralOverlayComponent 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay.component.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/general-overlay/general-overlay.component.scss -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GeneralOverlayComponent } from './general-overlay.component'; 4 | 5 | describe('GeneralOverlayComponent', () => { 6 | let component: GeneralOverlayComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ GeneralOverlayComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(GeneralOverlayComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; 2 | import OBSWebSocket from 'obs-websocket-js'; 3 | import { Subscription } from 'rxjs'; 4 | import { TwitchEventsService, TwitchEvent } from 'src/app/services/twitch-events.service'; 5 | 6 | const IGNORE_SCENES = ['Alert']; 7 | 8 | @Component({ 9 | selector: 'app-general-overlay', 10 | templateUrl: './general-overlay.component.html', 11 | styleUrls: ['./general-overlay.component.scss'] 12 | }) 13 | export class GeneralOverlayComponent implements OnInit { 14 | obs = new OBSWebSocket(); 15 | obsReady = false; 16 | scene = ''; 17 | subs = new Subscription(); 18 | eventQueue: TwitchEvent[] = []; 19 | 20 | currentMessage = ''; 21 | currentOverlay = ''; 22 | 23 | constructor( 24 | private twitchEvents: TwitchEventsService, 25 | private cdr: ChangeDetectorRef, 26 | ) { } 27 | 28 | ngOnInit(): void { 29 | this.twitchEvents.connect(); 30 | 31 | this.obs.connect({ address: 'localhost:4444'}).then(() => { 32 | console.log('Connection to OBS!'); 33 | return; 34 | }).then(() => { 35 | this.obsReady = true; 36 | return this.obs.send('GetSceneList'); 37 | }).then((data) => { 38 | this.scene = data['current-scene']; 39 | console.log(`The current scene is ${this.scene}`); 40 | }); 41 | this.obs.on('SwitchScenes', data => { 42 | this.scene = data['scene-name']; 43 | console.log(`The current scene is ${this.scene}`); 44 | }); 45 | 46 | this.subs.add(this.twitchEvents.message.subscribe(msg => { 47 | console.log(msg); 48 | if(!msg || IGNORE_SCENES.includes(this.scene)) { 49 | return; 50 | } 51 | if(msg.event_type === 'follow') { 52 | this.eventQueue.push({overlay: 'follow', eventData: msg, blur: false}); 53 | } 54 | if(this.eventQueue.length === 1) { 55 | this.checkEventQueue(true); 56 | } 57 | })); 58 | } 59 | checkEventQueue(originalEvent: boolean) { 60 | console.log(this.eventQueue); 61 | if(this.eventQueue.length === 1 && originalEvent) { 62 | const event = this.eventQueue[0]; 63 | this.clearCurrentEvent(); 64 | this.currentMessage = event.eventData; 65 | this.currentOverlay = event.overlay; 66 | } else if(this.eventQueue.length > 0 && !originalEvent) { 67 | const event = this.eventQueue[0]; 68 | this.clearCurrentEvent(); 69 | this.currentMessage = event.eventData; 70 | this.currentOverlay = event.overlay; 71 | } else if(this.eventQueue.length === 0) { 72 | this.clearCurrentEvent(); 73 | } 74 | } 75 | 76 | clearCurrentEvent() { 77 | this.currentMessage = ''; 78 | this.currentOverlay = ''; 79 | this.cdr.detectChanges(); 80 | } 81 | 82 | displayComplete() { 83 | console.log('Display Complete') 84 | console.log(this.eventQueue); 85 | this.eventQueue.shift(); 86 | this.checkEventQueue(false); 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/app/features/general-overlay/general-overlay.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { GeneralOverlayComponent } from './general-overlay.component'; 4 | import { RouterModule } from '@angular/router'; 5 | import { GeneralOverlayRoutes } from './general-overlay-routing.module'; 6 | import { FollowComponent } from './components/follow/follow.component'; 7 | 8 | 9 | 10 | @NgModule({ 11 | declarations: [ 12 | GeneralOverlayComponent, 13 | FollowComponent 14 | ], 15 | imports: [ 16 | CommonModule, 17 | RouterModule.forChild(GeneralOverlayRoutes) 18 | ] 19 | }) 20 | export class GeneralOverlayModule { } 21 | -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { ToggleChannelPointsComponent } from './toggle-channel-points.component'; 3 | 4 | export const ToggleChannelPointsRoutes: Routes = [ 5 | { 6 | path: '', 7 | pathMatch: 'full', 8 | component: ToggleChannelPointsComponent, 9 | }, 10 | ]; 11 | -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points.component.html: -------------------------------------------------------------------------------- 1 |

Toggle Rewards

2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/app/features/toggle-channel-points/toggle-channel-points.component.scss -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ToggleChannelPointsComponent } from './toggle-channel-points.component'; 4 | 5 | describe('ToggleChannelPointsComponent', () => { 6 | let component: ToggleChannelPointsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ToggleChannelPointsComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ToggleChannelPointsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { TwitchEventsService } from 'src/app/services/twitch-events.service'; 3 | 4 | const broadcasterId = '536397236'; 5 | 6 | const rewards = [ 7 | { 8 | id: '575c2991-cbc2-402c-837f-86bd40009379', 9 | name: 'writing wall', 10 | cost: 200, 11 | }, 12 | { 13 | id: '146beb92-ed1e-4845-8604-3408ebabf411', 14 | name: 'capt jack', 15 | cost: 200, 16 | }, 17 | { 18 | id: '99ef7c31-be4b-463f-8059-92c80f98ef32', 19 | name: 'water', 20 | cost: 100, 21 | }, 22 | { 23 | id: '4fd2137c-8dd6-411b-b406-a077ce017d0f', 24 | name: 'behind you', 25 | cost: 100, 26 | }, 27 | ]; 28 | 29 | @Component({ 30 | selector: 'app-toggle-channel-points', 31 | templateUrl: './toggle-channel-points.component.html', 32 | styleUrls: ['./toggle-channel-points.component.scss'], 33 | }) 34 | export class ToggleChannelPointsComponent implements OnInit { 35 | constructor(private twitchEvents: TwitchEventsService) {} 36 | 37 | ngOnInit(): void {} 38 | 39 | turnOff() { 40 | for (const reward of rewards) { 41 | this.twitchEvents 42 | .setRewardCost(broadcasterId, reward.id, 1000000) 43 | .subscribe((res) => console.log(res)); 44 | } 45 | } 46 | 47 | turnOn() { 48 | for (const reward of rewards) { 49 | this.twitchEvents 50 | .setRewardCost(broadcasterId, reward.id, reward.cost) 51 | .subscribe((res) => console.log(res)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/features/toggle-channel-points/toggle-channel-points.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { ToggleChannelPointsComponent } from './toggle-channel-points.component'; 5 | import { ToggleChannelPointsRoutes } from './toggle-channel-points-routing.module'; 6 | 7 | @NgModule({ 8 | declarations: [ToggleChannelPointsComponent], 9 | imports: [CommonModule, RouterModule.forChild(ToggleChannelPointsRoutes)], 10 | }) 11 | export class ToggleChannelPointsModule {} 12 | -------------------------------------------------------------------------------- /src/app/models/boom-circle.ts: -------------------------------------------------------------------------------- 1 | import { Circle } from "./circle"; 2 | 3 | export interface BoomCircle { 4 | maxRadius: number; 5 | circle: Circle; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/bounding-box.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./point"; 2 | 3 | export interface BoundingBox { 4 | minBound: Point; // Upper Left Corner 5 | maxBound: Point; // Lower Right Corner. 6 | } 7 | -------------------------------------------------------------------------------- /src/app/models/circle.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "./point"; 2 | 3 | export interface Circle { 4 | center: Point, 5 | r: number; 6 | color: [number, number, number]; 7 | } 8 | -------------------------------------------------------------------------------- /src/app/models/emote-message.ts: -------------------------------------------------------------------------------- 1 | export interface EmoteData { 2 | id: string; 3 | positions: [number, number][] 4 | } 5 | 6 | export interface EmoteMessage { 7 | text: string; 8 | emotes: EmoteData[]; 9 | } 10 | 11 | export interface MatrixMessageRow { 12 | mc_type: string; 13 | value: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/app/models/point.ts: -------------------------------------------------------------------------------- 1 | export interface Point { 2 | x: number; 3 | y: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/services/obs-websockets.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { ObsWebsocketsService } from './obs-websockets.service'; 4 | 5 | describe('ObsWebsocketsService', () => { 6 | let service: ObsWebsocketsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(ObsWebsocketsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/obs-websockets.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, Observable } from 'rxjs'; 3 | import { delay, retryWhen } from 'rxjs/operators'; 4 | import { webSocket } from 'rxjs/webSocket'; 5 | 6 | const OBSWS_ENDPOINT = 'ws://localhost:4444/'; 7 | 8 | 9 | export const startAdvKeys = { 10 | keyId: 'OBS_KEY_1', 11 | keyModifiers: { 12 | shift: true, 13 | alt: true, 14 | control: true, 15 | command: false, 16 | } 17 | }; 18 | export const stopAdvKeys = { 19 | keyId: 'OBS_KEY_2', 20 | keyModifiers: { 21 | shift: true, 22 | alt: true, 23 | control: true, 24 | command: false, 25 | } 26 | }; 27 | 28 | @Injectable({ 29 | providedIn: 'root' 30 | }) 31 | export class ObsWebsocketsService { 32 | private webSocket: any = null; 33 | private message$: BehaviorSubject = new BehaviorSubject(null); 34 | 35 | constructor() { 36 | } 37 | 38 | get message(): Observable { 39 | return this.message$.asObservable(); 40 | } 41 | 42 | public connect(): void { 43 | if (!this.webSocket || this.webSocket.closed) { 44 | this.webSocket = this.getNewWebSocket(); 45 | this.webSocket.pipe( 46 | // If we are disconnected, wait 2000ms before attempting to reconnect. 47 | retryWhen((err) => { 48 | console.log("Disconnected! Attempting reconnection shortly...") 49 | return err.pipe(delay(2000)); 50 | }) 51 | ).subscribe( 52 | // Once we receieve a message from the server, pass it to the handler function. 53 | (msg: any) => { 54 | this.handler(msg); 55 | }, 56 | ); 57 | } 58 | } 59 | 60 | getNewWebSocket() { 61 | return webSocket({ 62 | url: OBSWS_ENDPOINT, 63 | openObserver: { 64 | next: () => { 65 | console.log(`Connected to websocket at ${OBSWS_ENDPOINT}`); 66 | } 67 | }, 68 | }); 69 | } 70 | 71 | sendMessage(msg: any) { 72 | this.webSocket.next(msg); 73 | } 74 | 75 | close() { 76 | 77 | } 78 | 79 | handler(msg: any) { 80 | this.message$.next(msg); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/services/twitch-events.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | 3 | import { TwitchEventsService } from './twitch-events.service'; 4 | 5 | describe('TwitchEventsService', () => { 6 | let service: TwitchEventsService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({}); 10 | service = TestBed.inject(TwitchEventsService); 11 | }); 12 | 13 | it('should be created', () => { 14 | expect(service).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/services/twitch-events.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { webSocket } from 'rxjs/webSocket'; 3 | import { retryWhen, delay } from 'rxjs/operators'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | 6 | import { HttpClient } from '@angular/common/http'; 7 | 8 | // Local TAU endpoint. Token is tied to that instance of TAU. 9 | // Should move this to an environment file. 10 | //const WS_ENDPOINT = 'ws://localhost:8005/ws/twitch-events/'; 11 | //const token = '3e5c7c7f7535dcc0cc1206df5603208e3e26a100'; 12 | const WS_ENDPOINT = 'ws://localhost:8005/ws/twitch-events/'; 13 | const token = '3e5c7c7f7535dcc0cc1206df5603208e3e26a100'; 14 | 15 | export interface TwitchEvent { 16 | overlay: string; 17 | eventData: any; 18 | extraData?: any; 19 | blur: boolean; 20 | } 21 | 22 | @Injectable({ 23 | providedIn: 'root', 24 | }) 25 | export class TwitchEventsService { 26 | private webSocket: any = null; 27 | private message$: BehaviorSubject = new BehaviorSubject(null); 28 | 29 | constructor(private http: HttpClient) {} 30 | 31 | get message(): Observable { 32 | return this.message$.asObservable(); 33 | } 34 | 35 | public connect(): void { 36 | if (!this.webSocket || this.webSocket.closed) { 37 | this.webSocket = this.getNewWebSocket(); 38 | this.webSocket 39 | .pipe( 40 | // If we are disconnected, wait 2000ms before attempting to reconnect. 41 | retryWhen((err) => { 42 | console.log('Disconnected! Attempting reconnection shortly...'); 43 | return err.pipe(delay(2000)); 44 | }) 45 | ) 46 | .subscribe( 47 | // Once we receieve a message from the server, pass it to the handler function. 48 | (msg: any) => { 49 | this.handler(msg); 50 | } 51 | ); 52 | } 53 | } 54 | 55 | getNewWebSocket() { 56 | return webSocket({ 57 | url: WS_ENDPOINT, 58 | openObserver: { 59 | next: () => { 60 | console.log(`Connected to websocket at ${WS_ENDPOINT}`); 61 | this.sendMessage({ token }); 62 | }, 63 | }, 64 | }); 65 | } 66 | 67 | sendMessage(msg: any) { 68 | this.webSocket.next(msg); 69 | } 70 | 71 | close() {} 72 | 73 | handler(msg: any) { 74 | this.message$.next(msg); 75 | } 76 | 77 | getTwitchUserData(username: string) { 78 | const options = { 79 | headers: { 80 | Authorization: `Token ${token}`, 81 | }, 82 | }; 83 | console.log(options); 84 | return this.http.get( 85 | `http://localhost:8005/api/twitch/helix/users?login=${username}`, 86 | { 87 | headers: { Authorization: `Token ${token}` }, 88 | } 89 | ); 90 | } 91 | 92 | setRewardCost(broadcaster_id, reward_id, cost) { 93 | return this.http.patch( 94 | `http://localhost:8005/api/twitch/helix/channel_points/custom_rewards?broadcaster_id=${broadcaster_id}&id=${reward_id}`, 95 | { 96 | cost, 97 | }, 98 | { 99 | headers: { Authorization: `Token ${token}` }, 100 | } 101 | ); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/brush-bg-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/assets/brush-bg-1.png -------------------------------------------------------------------------------- /src/assets/brush-mask-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/assets/brush-mask-1.png -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FiniteSingularity/finitebot/8fa2b301a62000bde0f27e5960a9810b03b5a44a/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Finitebot 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | import { install } from 'paper'; 8 | 9 | if (environment.production) { 10 | enableProdMode(); 11 | } 12 | 13 | platformBrowserDynamic().bootstrapModule(AppModule) 14 | .catch(err => console.error(err)); 15 | 16 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | body { 4 | margin: 0px; 5 | overflow: hidden; 6 | } 7 | 8 | .erase { 9 | fill: none; 10 | stroke: #000000; 11 | stroke-width: 50px; 12 | 13 | /* Stroke-dasharray property */ 14 | stroke-dasharray: 1353px; 15 | stroke-dashoffset: 1353px; 16 | animation: move 1.4s linear; 17 | animation-fill-mode: forwards; 18 | } 19 | 20 | @keyframes move { 21 | 100% { 22 | stroke-dashoffset: 0; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": false, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "declaration": false, 13 | "downlevelIteration": true, 14 | "experimentalDecorators": true, 15 | "moduleResolution": "node", 16 | "importHelpers": true, 17 | "allowSyntheticDefaultImports": true, 18 | "target": "es2015", 19 | "module": "es2020", 20 | "lib": [ 21 | "es2018", 22 | "dom" 23 | ] 24 | }, 25 | "angularCompilerOptions": { 26 | "enableI18nLegacyMessageIdFormat": false, 27 | "strictInjectionParameters": true, 28 | "strictInputAccessModifiers": true, 29 | "strictTemplates": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | --------------------------------------------------------------------------------