diff options
feat: more things
Diffstat (limited to 'src/user/index.ts')
-rw-r--r-- | src/user/index.ts | 246 |
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 } |