aboutsummaryrefslogtreecommitdiffstats
path: root/src/user/index.ts
diff options
context:
space:
mode:
authorLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2025-08-02 13:25:30 +0200
committerLibravatarLarge Libravatar memdmp <memdmpestrogenzone>2025-08-02 13:25:30 +0200
commit9ab2e2884b956bc79c01d6edfe867a85c2870796 (patch)
treee9a28447afc245c194e3d43c81b9d3ffd18bc55b /src/user/index.ts
parent4c86299e5759e2234ac1856690dadbdadce2ec2a (diff)
downloadvideotool-9ab2e2884b956bc79c01d6edfe867a85c2870796.tar.gz
videotool-9ab2e2884b956bc79c01d6edfe867a85c2870796.tar.bz2
videotool-9ab2e2884b956bc79c01d6edfe867a85c2870796.tar.lz
videotool-9ab2e2884b956bc79c01d6edfe867a85c2870796.zip

feat: more things

Diffstat (limited to 'src/user/index.ts')
-rw-r--r--src/user/index.ts246
1 files changed, 207 insertions, 39 deletions
diff --git a/src/user/index.ts b/src/user/index.ts
index fb0001e..541cd94 100644
--- a/src/user/index.ts
+++ b/src/user/index.ts
@@ -1,6 +1,8 @@
import { type FrameTime, type InitConfig } from '$/lib/Player/Video';
-import ThreeVideo from './ThreeVideo';
-import AudioURL from './03. Lemaitre, Jennie A. - Closer - 2min version.flac?url'
+import ThreeVideo, { OnceCell } from './ThreeVideo';
+import AudioURL from './03. Lemaitre, Jennie A. - Closer - 40sec version.flac?url'
+import * as THREE from 'three';
+import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';
type FontInfo = {
family: string, size: number, weight?: number
@@ -16,7 +18,21 @@ const getTextSize = (ctx: CanvasRenderingContext2D, text: string, fontInfo: Font
return ctx.measureText(text)
}
+const lerp = (t: number, initial: number, final: number) => {
+ return initial + ((final - initial) * t)
+}
+const bezier = (t: number, initial: number, p1: number, p2: number, final: number) => {
+ return (1 - t) * (1 - t) * (1 - t) * initial
+ +
+ 3 * (1 - t) * (1 - t) * t * p1
+ +
+ 3 * (1 - t) * t * t * p2
+ +
+ t * t * t * final;
+}
+
export default class Video extends ThreeVideo {
+ protected ctx!: CanvasRenderingContext2D
protected isPreview = false;
protected px(pixels: number) {
return this.isPreview ? pixels / 1.5 : pixels
@@ -25,6 +41,10 @@ export default class Video extends ThreeVideo {
const { isPreview } = config
this.isPreview = isPreview
this.resize(this.px(1920), this.px(1080))
+ this.ctx = this.canvas.getContext('2d', {
+ willReadFrequently: !isPreview,
+ desynchronized: isPreview,
+ })!
const threeInit = super.init(config).catch(e => ([1, e] as const))
// const v = document.createElement('video')
@@ -44,47 +64,195 @@ export default class Video extends ThreeVideo {
const rs = await threeInit
if (rs && rs[0] === 1) { console.error(rs[1]); throw new Error('Failed to initialize ThreeJS!'); }
}
+ protected uiGeometry = OnceCell(() => new THREE.BoxGeometry(0.1, 5, 7));
+ protected uiDarkMaterial = OnceCell(() => new THREE.MeshStandardMaterial({
+ roughness: 0.8,
+ color: 0xffffff,
+ metalness: 0.2,
+ bumpScale: 1
+ }));
+ protected uiDark = OnceCell(() => new THREE.Mesh(this.uiGeometry(), this.uiDarkMaterial()))
+ protected uiLightMaterial = OnceCell(() => new THREE.MeshBasicMaterial({ color: 0x000000 }))
+ protected uiLight = OnceCell(() => new THREE.Mesh(this.uiGeometry(), this.uiLightMaterial()))
+ protected lighting = OnceCell(() => {
+ const dirLight = new THREE.DirectionalLight(0xffffff, 3);
+ dirLight.castShadow = true;
+ dirLight.shadow.camera.top = 0;
+ dirLight.shadow.camera.bottom = 0;
+ dirLight.shadow.camera.left = 0;
+ dirLight.shadow.camera.right = 0;
+ dirLight.shadow.camera.near = 0.1;
+ dirLight.shadow.camera.far = 90;
+
+ const cam = dirLight.shadow.camera;
+ cam.top = cam.right = 0;
+ cam.bottom = cam.left = 0;
+ cam.near = 3;
+ cam.far = 8;
+ dirLight.shadow.mapSize.set(1024, 1024);
+
+ return dirLight;
+ })
+ protected uiCanvas = OnceCell(() => {
+ const c = document.createElement('canvas');
+ c.width = this.px(1000);
+ c.height = c.width / 5 * 7;
+ return c;
+ });
+ protected uiCanvasCtx = OnceCell(() => this.uiCanvas().getContext('2d', {
+ alpha: true,
+ }))
+ protected drawUiCanvas() { }
public renderFrame(time: FrameTime): Promise<void> | void {
const beat = 1 + ((time.seconds - 0.098) * (92 / 60))
const center = [this.w / 2, this.h / 2] as const
- // if (beat < 13 || beat >= 16) {
- // this.ctx.fillStyle = '#fff';
- // this.ctx.fillRect(0, 0, this.w, this.h);
- // } else {
- // this.ctx.fillStyle = '#000';
- // this.ctx.fillRect(0, 0, this.w, this.h);
- // }
- // const AdDefault: FontInfo = {
- // family: 'Inter Variable',
- // size: this.px(52),
- // weight: 450
- // }
- // if (this.isPreview)
- // renderText(this.ctx, `${(Math.floor(beat * 100) / 100).toFixed(1)}`, '#646663', { ...AdDefault, size: this.px(12), weight: 400 }, { x: this.w - this.px(4), y: this.h - this.px(4) }, 'end')
- // switch (true) {
- // case beat >= 1 && beat < 4.3:
- // renderText(this.ctx, `Need a new AI assistant?`, '#646663', AdDefault, { x: center[0], y: center[1] }, 'center')
- // break;
- // case beat >= 4.3 && beat < 8.4: {
- // const text = `Like${beat >= 4.8 ? ' new' : ''}${beat >= 5.02 ? ' new' : ''}`;
- // const longTextWidth = getTextSize(this.ctx, 'Like new new', AdDefault)
- // renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
- // break;
- // }
- // case beat >= 8.4 && beat < 13: {
- // const text = `Like${beat >= 9 ? ` runs random shit in your terminal` : ''}${beat >= 11 ? ' new' : ''}`;
- // const longTextWidth = getTextSize(this.ctx, `Like runs random shit in your terminal new`, AdDefault)
- // renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
- // break;
- // }
- // // TODO:
+ this.ctx.fillStyle = '#fff';
+ this.ctx.fillRect(0, 0, this.w, this.h);
+
+ this.scene.background = null;
+
+ const AdDefault: FontInfo = {
+ // family: 'Inter Variable',
+ family: 'Adwaita Sans',
+ size: this.px(58),
+ weight: 450
+ }
+ switch (true) {
+ case beat < 1:
+ break;
+ case beat >= 1 && beat < 4.3:
+ renderText(this.ctx, `Need a new AI assistant?`, '#646663', AdDefault, { x: center[0], y: center[1] }, 'center')
+ break;
+ case beat >= 4.3 && beat < 8.4: {
+ const text = `Like${beat >= 4.8 ? ' new' : ''}${beat >= 5.02 ? ' new?' : ''}`;
+ const longTextWidth = getTextSize(this.ctx, 'Like new new?', AdDefault)
+ renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ break;
+ }
+ case beat >= 8.4 && beat < 13: {
+ const text = `Like${beat >= 9 ? ` has a hyprminimal design` : ''}${beat >= 11 ? ' new' : ''}`;
+ const longTextWidth = getTextSize(this.ctx, `Like has a hyprminimal design new`, AdDefault)
+ renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ break;
+ }
+ case beat >= 13 && beat < 15: {
+ this.scene.background = new THREE.Color(0x000000);
- // default:
- // break;
- // }
+ this.scene.add(this.lighting())
+
+ this.scene.add(this.uiDark());
+ const progress = (beat - 13) / 3.5
+ this.uiDark().rotation.x = bezier(progress, 0.4, 0.6, 0.6, 1.1);
+ this.uiDark().rotation.y = bezier(progress, 2, 1.7, 1.6, bezier(progress, 1, 0.8, 0.3, -0.5));
+ this.camera.position.z = bezier(progress, 6, 4, 4, bezier(progress, 7, 9, 15, 25));
+ this.lighting().position.set(0, 0, this.camera.position.z);
+ this.renderScene(this.ctx)
+ this.scene.remove(this.uiDark());
+ this.scene.remove(this.lighting())
+ break;
+ }
+ case beat >= 15 && beat < 16: {
+ this.scene.background = new THREE.Color(0x000000);
+
+ this.scene.add(this.lighting())
+
+ this.scene.add(this.uiDark());
+ const progress = (beat - 15)
+ this.uiDark().rotation.x = bezier(progress, -0.5, -0.5, -0.5, -0.5);
+ this.uiDark().rotation.y = bezier(progress, 0.7, 0.6, 0.4, 0.4);
+ this.camera.position.z = bezier(progress, 4, 5, 5, 8);
+ this.lighting().position.set(-0.5, 0, this.camera.position.z);
+ this.renderScene(this.ctx)
+ this.scene.remove(this.uiDark());
+ this.scene.remove(this.lighting())
+ break;
+ }
+ case beat >= 16 && beat < 18.4: {
+ this.scene.add(this.uiLight());
+ const progress = (beat - 16) / 3.5
+ this.uiLight().rotation.x = bezier(progress, -0.5, 0.6, 0.6, 1.1) * (beat < 17 ? 1 : -1);
+ this.uiLight().rotation.y = bezier(progress, 0.4, 1.7, 1.8, 2) * (beat < 17 ? 1 : -1);
+ this.camera.position.z = bezier(progress, 8, 4, 4, 12);
+
+ this.renderScene(this.ctx)
+ this.scene.remove(this.uiLight());
+ break;
+ }
+ case beat >= 18.4 && beat < 22.8: {
+ const text = `Efficiency ${beat >= 19 ? `so hyprefficient, ` : ''}${beat >= 20 ? 'we created it' : ''}${beat >= 21 ? ' hypr' + (beat >= 22.1 ? 'new' : '') : ''}`;
+ const longTextWidth = getTextSize(this.ctx, `Efficiency so hyprefficient, we created it hyprnew`, AdDefault)
+ renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ break;
+ }
+ case beat >= 22.8 && beat < 25:
+ // TODO: add animation for hyprefficient
+ renderText(this.ctx, `TODO`, '#ff000099', AdDefault, { x: center[0], y: center[1] }, 'center')
+ break;
+ case beat >= 25 && beat < 31 || beat < 33: {
+ const text = `1 month free ${beat >= 26.5 ? 'for a hypr' : ''}${beat >= 26.7 ? 'local Mistral 7b' : ''}${beat >= 29 ? ' new' : ''}`;
+ const longTextWidth = getTextSize(this.ctx, `1 month free for a hyprlocal Mistral 7b new`, AdDefault)
+ renderText(this.ctx, text, '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ renderText(this.ctx, '*Not guaranteed to work. Subscriptions start at 69.99CHF/mo and get billed on the first of the next month automatically.', '#64666366', { ...AdDefault, size: 12, weight: 400 }, { x: center[0], y: this.h - this.px(44) }, 'center')
+ renderText(this.ctx, 'Cancellable only within 12 hours of the first day of the month.', '#64666366', { ...AdDefault, size: 12, weight: 400 }, { x: center[0], y: this.h - this.px(24) }, 'center')
+ break;
+ }
+ case beat >= 31 && beat < 33://&& beat < 33:
+ // TODO: add animation for hyprsubscription
+ renderText(this.ctx, `i've already dumped too much time into this pls contribute`, '#ff000099', AdDefault, { x: center[0], y: center[1] }, 'center')
+ break;
+ case beat >= 33 && beat < 41: {
+ if (beat >= 34.75) {
+ this.ctx.fillStyle = '#23f';
+ this.ctx.fillRect(0, 0, this.w, this.h);
+ // renderText(this.ctx, `Need a new AI assistant?`, '#646663', AdDefault, { x: center[0], y: center[1] }, 'center')
+ }
+ const text = `Accents ${beat >= 34.5 ? `so ` : ''}${beat >= 34.75 ? 'hyprblue, ' : ''}${beat >= 35.8 ? 'we created them' : ''}${beat >= 37 ? ' hypr' + (beat >= 38.1 ? 'new' : '') : ''}`;
+ const longTextWidth = getTextSize(this.ctx, `Accents so hyprblue, we created them hyprnew`, AdDefault)
+ renderText(this.ctx, text, beat >= 34.75 ? '#ffffff' : '#646663', AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ break;
+ }
+
+ case beat >= 49 && beat < 56.8: {
+ const opacity1 = Math.min(Math.max(Math.floor(255 - lerp((beat - 50) * 1.1, 255, 0)), 0), 255).toString(16).padStart(2, '0')
+ const opacity2 = Math.min(Math.max(Math.floor(255 - lerp((beat - 52.7) * 1.1, 255, 0)), 0), 255).toString(16).padStart(2, '0')
+ const text1 = `Introducing `;
+ const text2 = `HyprAI`;
+ const longTextWidth = getTextSize(this.ctx, `Introducing HyprAI`, AdDefault)
+ const shortTextWidth = getTextSize(this.ctx, `Introducing `, AdDefault)
+ renderText(this.ctx, text1, '#646663' + opacity1, AdDefault, { x: center[0] - longTextWidth.width / 2, y: center[1] }, 'start')
+ renderText(this.ctx, text2, '#646663' + opacity2, AdDefault, { x: (center[0] - longTextWidth.width / 2) + shortTextWidth.width, y: center[1] }, 'start')
+ break;
+ }
+ case beat >= 56.8 && beat < 67: {
+ renderText(this.ctx, beat < 61 ? `AI assistant by Hyprland` : 'Who knew?', '#646663', AdDefault, { x: center[0], y: center[1] }, 'center')
+ if (beat < 61)
+ renderText(this.ctx, `*This ad is satire vaxry please don't sue me`, '#64666366', {
+ ...AdDefault,
+ size: 12,
+ weight: 400
+ }, { x: center[0], y: this.h - this.px(24) }, 'center')
+ // fall-thru
+ }
+ case beat >= 64: {
+ if (beat >= 64) {
+ // needed due to fallthru
+ // TODO: animate ui going up over text
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ if (this.isPreview)
+ renderText(this.ctx, `${(Math.floor(beat * 10) / 10).toFixed(1)}`, '#646663', { ...AdDefault, size: this.px(12), weight: 400 }, { x: this.w - this.px(4), y: this.h - this.px(4) }, 'end')
+ }
+ public cleanup(): void {
+ this.uiCanvas().remove()
+ super.cleanup()
}
- // public fps = 59.94;
- public fps = 30;
- public length = Math.ceil(2 * 60 * this.fps);
+ public fps = 59.94;
+ // public fps = 119.88;
+ // public fps = 30;
+ public length = Math.ceil(43.5 * this.fps);
public audioUrl = ['03. Lemaitre, Jennie A. - Closer.flac', AudioURL] as const
}