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 { 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 { 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 = 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 }