aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--deno.lock4
-rw-r--r--package.json4
-rw-r--r--pnpm-lock.yaml59
-rw-r--r--src/user/.gitignore1
-rw-r--r--src/user/Sneky Snitch.mp4bin1620945 -> 0 bytes
-rw-r--r--src/user/ThreeVideo.ts53
-rw-r--r--src/user/index.ts271
7 files changed, 374 insertions, 18 deletions
diff --git a/deno.lock b/deno.lock
index df9d81e..b88a794 100644
--- a/deno.lock
+++ b/deno.lock
@@ -1018,16 +1018,20 @@
],
"packageJson": {
"dependencies": [
+ "npm:@ffmpeg/ffmpeg@~0.12.15",
+ "npm:@ffmpeg/util@~0.12.2",
"npm:@sveltejs/adapter-static@3.0.8",
"npm:@sveltejs/kit@2.19.0",
"npm:@sveltejs/vite-plugin-svelte@5.0.3",
"npm:@tailwindcss/vite@4.1.11",
+ "npm:@types/three@~0.178.1",
"npm:esbuild@0.25.1",
"npm:prettier-plugin-svelte@3.3.3",
"npm:prettier@3.5.3",
"npm:svelte-check@4.1.5",
"npm:svelte@5.23.0",
"npm:tailwindcss@4.0.13",
+ "npm:three@0.178",
"npm:typescript@5.8.2",
"npm:vite@6.2.1"
]
diff --git a/package.json b/package.json
index a6c9f48..1a48225 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"@sveltejs/kit": "2.19.0",
"@sveltejs/vite-plugin-svelte": "5.0.3",
"@tailwindcss/vite": "4.1.11",
+ "@types/three": "^0.178.1",
"esbuild": "0.25.1",
"prettier": "3.5.3",
"prettier-plugin-svelte": "3.3.3",
@@ -19,6 +20,7 @@
},
"dependencies": {
"@ffmpeg/ffmpeg": "^0.12.15",
- "@ffmpeg/util": "^0.12.2"
+ "@ffmpeg/util": "^0.12.2",
+ "three": "^0.178.0"
}
} \ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index eba1dcc..7ca5702 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ importers:
'@ffmpeg/util':
specifier: ^0.12.2
version: 0.12.2
+ three:
+ specifier: ^0.178.0
+ version: 0.178.0
devDependencies:
'@sveltejs/adapter-static':
specifier: 3.0.8
@@ -27,6 +30,9 @@ importers:
'@tailwindcss/vite':
specifier: 4.1.11
version: 4.1.11(vite@6.2.1(jiti@2.4.2)(lightningcss@1.30.1))
+ '@types/three':
+ specifier: ^0.178.1
+ version: 0.178.1
esbuild:
specifier: 0.25.1
version: 0.25.1
@@ -58,6 +64,9 @@ packages:
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
engines: {node: '>=6.0.0'}
+ '@dimforge/rapier3d-compat@0.12.0':
+ resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==}
+
'@esbuild/aix-ppc64@0.25.1':
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
engines: {node: '>=18'}
@@ -464,12 +473,27 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7
+ '@tweenjs/tween.js@23.1.3':
+ resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==}
+
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
+ '@types/stats.js@0.17.4':
+ resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==}
+
+ '@types/three@0.178.1':
+ resolution: {integrity: sha512-WSabew1mgWgRx2RfLfKY+9h4wyg6U94JfLbZEGU245j/WY2kXqU0MUfghS+3AYMV5ET1VlILAgpy77cB6a3Itw==}
+
+ '@types/webxr@0.5.22':
+ resolution: {integrity: sha512-Vr6Stjv5jPRqH690f5I5GLjVk8GSsoQSYJ2FVd/3jJF7KaqfwPi3ehfBS96mlQ2kPCwZaX6U0rG2+NGHBKkA/A==}
+
+ '@webgpu/types@0.1.64':
+ resolution: {integrity: sha512-84kRIAGV46LJTlJZWxShiOrNL30A+9KokD7RB3dRCIqODFjodS5tCD5yyiZ8kIReGVZSDfA3XkkwyyOIF6K62A==}
+
acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'}
@@ -542,6 +566,9 @@ packages:
picomatch:
optional: true
+ fflate@0.8.2:
+ resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -634,6 +661,9 @@ packages:
magic-string@0.30.17:
resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==}
+ meshoptimizer@0.18.1:
+ resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==}
+
minipass@7.1.2:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -731,6 +761,9 @@ packages:
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
engines: {node: '>=18'}
+ three@0.178.0:
+ resolution: {integrity: sha512-ybFIB0+x8mz0wnZgSGy2MO/WCO6xZhQSZnmfytSPyNpM0sBafGRVhdaj+erYh5U+RhQOAg/eXqw5uVDiM2BjhQ==}
+
totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -802,6 +835,8 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.8
'@jridgewell/trace-mapping': 0.3.25
+ '@dimforge/rapier3d-compat@0.12.0': {}
+
'@esbuild/aix-ppc64@0.25.1':
optional: true
@@ -1083,10 +1118,28 @@ snapshots:
tailwindcss: 4.1.11
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.30.1)
+ '@tweenjs/tween.js@23.1.3': {}
+
'@types/cookie@0.6.0': {}
'@types/estree@1.0.6': {}
+ '@types/stats.js@0.17.4': {}
+
+ '@types/three@0.178.1':
+ dependencies:
+ '@dimforge/rapier3d-compat': 0.12.0
+ '@tweenjs/tween.js': 23.1.3
+ '@types/stats.js': 0.17.4
+ '@types/webxr': 0.5.22
+ '@webgpu/types': 0.1.64
+ fflate: 0.8.2
+ meshoptimizer: 0.18.1
+
+ '@types/webxr@0.5.22': {}
+
+ '@webgpu/types@0.1.64': {}
+
acorn@8.14.1: {}
aria-query@5.3.2: {}
@@ -1154,6 +1207,8 @@ snapshots:
fdir@6.4.3: {}
+ fflate@0.8.2: {}
+
fsevents@2.3.3:
optional: true
@@ -1220,6 +1275,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ meshoptimizer@0.18.1: {}
+
minipass@7.1.2: {}
minizlib@3.0.2:
@@ -1336,6 +1393,8 @@ snapshots:
mkdirp: 3.0.1
yallist: 5.0.0
+ three@0.178.0: {}
+
totalist@3.0.1: {}
typescript@5.8.2: {}
diff --git a/src/user/.gitignore b/src/user/.gitignore
new file mode 100644
index 0000000..345a096
--- /dev/null
+++ b/src/user/.gitignore
@@ -0,0 +1 @@
+/*.flac
diff --git a/src/user/Sneky Snitch.mp4 b/src/user/Sneky Snitch.mp4
deleted file mode 100644
index b9d53b5..0000000
--- a/src/user/Sneky Snitch.mp4
+++ /dev/null
Binary files differ
diff --git a/src/user/ThreeVideo.ts b/src/user/ThreeVideo.ts
new file mode 100644
index 0000000..92615f1
--- /dev/null
+++ b/src/user/ThreeVideo.ts
@@ -0,0 +1,53 @@
+import { Video as BaseVideo, type FrameTime, type InitConfig } from '$/lib/Player/Video';
+import * as THREE from 'three';
+import type { OrbitControls } from 'three/examples/jsm/Addons.js';
+
+export const OnceCell = <T>(create: () => T) => {
+ let called = false,
+ cached = null as unknown as T
+ return () => {
+ if (called) return cached
+ else { cached = create(); called = true; return cached }
+ }
+}
+
+export default abstract class ThreeVideo extends BaseVideo {
+ protected abstract ctx: CanvasRenderingContext2D
+ protected scene!: THREE.Scene;
+ protected camera!: THREE.PerspectiveCamera;
+ protected renderer!: THREE.WebGLRenderer;
+ protected threeCanvas!: HTMLCanvasElement;
+ protected orbitControls?: OrbitControls;
+ public async init(_config: InitConfig): Promise<void> {
+ this.scene = new THREE.Scene();
+ this.camera = new THREE.PerspectiveCamera(75, this.w / this.h, 0.1, 1000);
+
+ const canvas = this.threeCanvas = document.createElement('canvas');
+ canvas.width = this.canvas.width
+ canvas.height = this.canvas.height
+ canvas.style.opacity = "0"
+ canvas.style.position = "fixed";
+ canvas.style.top = "1000vh"
+ canvas.style.left = "1000vw"
+ document.body.appendChild(canvas)
+
+ this.renderer = new THREE.WebGLRenderer({
+ canvas: canvas,
+ alpha: true,
+ powerPreference: 'high-performance',
+ });
+ this.renderer.setSize(this.w, this.h);
+ this.renderer.setAnimationLoop(() => this.renderScene());
+ }
+ public renderScene(ctx?: CanvasRenderingContext2D) {
+ if (this.orbitControls)
+ this.orbitControls.update()
+ this.renderer.render(this.scene, this.camera)
+ if (ctx)
+ ctx.drawImage(this.threeCanvas, 0, 0)
+ }
+ public cleanup(): void {
+ this.renderer.dispose()
+ this.threeCanvas.remove()
+ }
+}
diff --git a/src/user/index.ts b/src/user/index.ts
index 2caf88c..541cd94 100644
--- a/src/user/index.ts
+++ b/src/user/index.ts
@@ -1,21 +1,258 @@
-import { Video as BaseVideo, type FrameTime } from '$/lib/Player/Video';
-import SneakySnitchUrl from './Sneky Snitch.mp4?url'
-
-export default class Video extends BaseVideo {
- public ctx!: CanvasRenderingContext2D
- public init(): void | Promise<void> {
- // this.resize(this.canvas.clientWidth,this.canvas.clientHeight)
- this.resize(1920, 1080)
- this.ctx = this.canvas.getContext('2d')!
+import { type FrameTime, type InitConfig } from '$/lib/Player/Video';
+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
+}
+const renderText = (ctx: CanvasRenderingContext2D, text: string, color: string, fontInfo: FontInfo, { x, y }: { x: number, y: number }, align = 'left' as CanvasTextAlign) => {
+ ctx.font = `normal normal ${fontInfo.weight ?? 400} ${fontInfo.size}px ${fontInfo.family}`;
+ ctx.fillStyle = color
+ ctx.textAlign = align
+ ctx.fillText(text, x, y)
+}
+const getTextSize = (ctx: CanvasRenderingContext2D, text: string, fontInfo: FontInfo) => {
+ ctx.font = `normal normal ${fontInfo.weight ?? 400} ${fontInfo.size}px ${fontInfo.family}`;
+ 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
}
+ public async init(config: InitConfig): Promise<void> {
+ 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')
+ // v.load()
+ // await new Promise((rs, rj) => {
+ // let debounce = false;
+ // const timeout = setTimeout(() => {
+ // if (!debounce) rj('Failed to load video - timed out.')
+ // }, 1000);
+ // v.addEventListener('load', () => {
+ // rs(void 0)
+ // clearTimeout(timeout)
+ // }, {
+ // once: true
+ // })
+ // })
+ 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 {
- this.ctx.fillStyle = '#000'
- this.ctx.fillRect(0, 0, this.w, this.h)
- this.ctx.font = "50px Nunito";
- this.ctx.fillStyle = '#fff'
- this.ctx.fillText(`${time.seconds.toFixed(3)}`, 0, 50)
+ const beat = 1 + ((time.seconds - 0.098) * (92 / 60))
+ const center = [this.w / 2, this.h / 2] as const
+ 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);
+
+ 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 = 30;
- public length = 3 * this.fps;
- public audioUrl = ['sneakysnitch.mp4', SneakySnitchUrl] as const
+ 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
}