diff --git a/index.html b/index.html index f6db8ae..674f064 100644 --- a/index.html +++ b/index.html @@ -191,6 +191,9 @@ +
+
+
diff --git a/package.json b/package.json index d023a1c..0dfb6e9 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,13 @@ "@web-media/event-target": "^0.0.2", "@web-media/phonograph": "2.3.2", "debug": "^4.3.4", + "moment": "^2.30.1", "standardized-audio-context": "^25.3.72", "stats.js": "^0.17.0", "three": "0.138.3", - "tslib": "^2.3.1" + "tslib": "^2.3.1", + "vis-data": "^7.1.9", + "vis-timeline": "^7.7.3" }, "devDependencies": { "@biomejs/biome": "1.7.3", diff --git a/src/components/Grass.ts b/src/components/Grass.ts index 537b4f5..acb852c 100644 --- a/src/components/Grass.ts +++ b/src/components/Grass.ts @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { THEME_VALUE } from '../manager/ColorManager'; import { timeManager } from '../manager/TimeManager'; import { VISUAL_LOAD } from '../stores/settings'; diff --git a/src/components/Ground2.ts b/src/components/Ground2.ts index 078be55..967b10e 100644 --- a/src/components/Ground2.ts +++ b/src/components/Ground2.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { CylinderGeometry2 } from './CylinderGeometry2'; export const RADIUS = 120; diff --git a/src/components/GroundObject.ts b/src/components/GroundObject.ts index 252806d..27d03da 100644 --- a/src/components/GroundObject.ts +++ b/src/components/GroundObject.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { timeManager } from '../manager/TimeManager'; import { GROUND_Y_OFFSET, RADIUS, SCALE_Z } from './Ground2'; diff --git a/src/components/SkyBox.ts b/src/components/SkyBox.ts index 73d5311..f2b70ee 100644 --- a/src/components/SkyBox.ts +++ b/src/components/SkyBox.ts @@ -1,5 +1,5 @@ import * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; export const SkyBox = (tracker: ResourceTracker) => { const geometry = new THREE.SphereGeometry(500, 20, 20); diff --git a/src/initializeThreeCanvas.ts b/src/initializeThreeCanvas.ts index 84aaa57..626fdea 100644 --- a/src/initializeThreeCanvas.ts +++ b/src/initializeThreeCanvas.ts @@ -9,7 +9,7 @@ import { SepiaShader } from 'three/examples/jsm/shaders/SepiaShader'; import { VignetteShader } from 'three/examples/jsm/shaders/VignetteShader'; import { CANVAS_SIZE, updateCanvasSize } from './stores/settings'; -import { ResourceTracker } from './ResourceTracker'; +import { ResourceTracker } from './utils/ResourceTracker'; import { timeManager } from './manager/TimeManager'; import { FrameRateLevel } from './utils/TimeMagic'; import { GlitchShader } from './utils/shaders/GlitchPass'; diff --git a/src/main.css b/src/main.css index d1fcca8..2267bb1 100644 --- a/src/main.css +++ b/src/main.css @@ -607,4 +607,56 @@ button { .disconnected { font-size: 1.3em; font-weight: 800; +} + +/** Other debugging tools **/ +.story-visualize-container { + left: 0; + bottom: 0; + width: 100%; + height: 30vh; + position: fixed; + background: rgba(0, 0, 0, 0.24); + backdrop-filter: blur(6px); +} + +.story-visualize-container canvas { + height: 100%; +} + +.vis-time-axis .vis-text { + color: white; + font-weight: 400; +} + +.vis-item-content { + font-size: 14px; + font-weight: 400; +} + +.vis-timeline { + border-color: rgba(255, 255, 255, 0.3); +} + +.vis-time-axis .vis-grid.vis-minor { + border-color: rgba(255, 255, 255, 0.3); +} + +.vis-panel.vis-center, .vis-panel.vis-left, .vis-panel.vis-right, .vis-panel.vis-top, .vis-panel.vis-bottom { + border-color: rgba(255, 255, 255, 0.3); +} + +.vis-item { + border-radius: 0; + background: rgba(0, 0, 0, 0.24); + border-image: linear-gradient(60deg, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.6)) 10%; + box-shadow: 0 0 0 rgba(255, 255, 255, 0.0), inset 0 0 0 rgba(255, 255, 255, 0); + color: white; +} + +.vis-item.vis-selected { + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.3); + box-shadow: 0 0 4px rgba(255, 255, 255, 0.5), inset 0 0 4px rgba(255, 255, 255, 0.5); + border-image: linear-gradient(60deg, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 1)) 10%; } \ No newline at end of file diff --git a/src/manager/DebugManager.ts b/src/manager/DebugManager.ts index 3ec1dd8..bac4638 100644 --- a/src/manager/DebugManager.ts +++ b/src/manager/DebugManager.ts @@ -1,6 +1,6 @@ import type * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; export const DebugManager = ( camera: THREE.Camera, diff --git a/src/manager/GrassManager.ts b/src/manager/GrassManager.ts index 883947a..9af9c11 100644 --- a/src/manager/GrassManager.ts +++ b/src/manager/GrassManager.ts @@ -1,6 +1,6 @@ import type * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { GRID_HEIGHT, GRID_WIDTH, diff --git a/src/manager/GroundManager2.ts b/src/manager/GroundManager2.ts index adef1ce..df2f44b 100644 --- a/src/manager/GroundManager2.ts +++ b/src/manager/GroundManager2.ts @@ -1,6 +1,6 @@ import * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import type { CylinderGeometry2 } from '../components/CylinderGeometry2'; import { Ground } from '../components/Ground2'; import { STEP_ANGLE } from '../constants/ground'; diff --git a/src/manager/GroundObjectManager.ts b/src/manager/GroundObjectManager.ts index 20b0f93..1a920b8 100644 --- a/src/manager/GroundObjectManager.ts +++ b/src/manager/GroundObjectManager.ts @@ -1,6 +1,6 @@ import type * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { GroundObject } from '../components/GroundObject'; import { STEP_ANGLE } from '../constants/ground'; import { CompressedTexture } from '../utils/CompressedTexture'; diff --git a/src/manager/SkyBoxManager.ts b/src/manager/SkyBoxManager.ts index 0a65f19..1fbcfb6 100644 --- a/src/manager/SkyBoxManager.ts +++ b/src/manager/SkyBoxManager.ts @@ -1,6 +1,6 @@ import type * as THREE from 'three'; -import type { ResourceTracker } from '../ResourceTracker'; +import type { ResourceTracker } from '../utils/ResourceTracker'; import { SkyBox } from '../components/SkyBox'; import { THEME_VALUE } from './ColorManager'; diff --git a/src/manager/StoryManager.ts b/src/manager/StoryManager.ts index 95a6989..22f1805 100644 --- a/src/manager/StoryManager.ts +++ b/src/manager/StoryManager.ts @@ -1,5 +1,7 @@ import debug from 'debug'; import { Clip, Mp3DeMuxAdapter } from '@web-media/phonograph'; +import { type DataItem, Timeline, type DataItemCollectionType, type TimelineOptions } from 'vis-timeline'; +import 'vis-timeline/dist/vis-timeline-graph2d.css'; import { THEME_ID } from './ColorManager'; @@ -13,6 +15,7 @@ import { QUERY_PARAMETER, ROUTER_ID, isStory } from '../stores/router'; import { type ITimelineEvent, TimelineManager } from '../utils/TimeLine'; import { timeManager } from './TimeManager'; +import { forceSelect } from '../utils/forceSelect'; export const STORY_AUDIO_URL_BASE = 'https://resource.alice.is.not.ci/'; @@ -22,66 +25,67 @@ const logEnd = log.extend('end'); const logTheme = log.extend('theme'); const logLowRpm = log.extend('low-rpm'); -export const timeLine = new TimelineManager( - stories, - { - audio: (x: ITimelineEvent<'audio', IPlayAudioStoryEvent>) => { - const clip = new Clip({ - context: globalAudioContext, - url: STORY_AUDIO_URL_BASE + x.detail.url, - adapter: new Mp3DeMuxAdapter(), - }); +export const timeLine = new TimelineManager(stories, { + audio: (x: ITimelineEvent<'audio', IPlayAudioStoryEvent>) => { + const clip = new Clip({ + context: globalAudioContext, + url: STORY_AUDIO_URL_BASE + x.detail.url, + adapter: new Mp3DeMuxAdapter(), + }); - const id = x.detail.url; + const id = x.detail.url; - logAudio(`[load] [start] ${id}`); - clip.canPlayThough.then((x) => { - logAudio(`[play] [start] ${id}`); - clip.play().then((x) => { - logAudio(`[play] [ end ] ${id}`); - }); + logAudio(`[load] [start] ${id}`); + clip.canPlayThough.then((x) => { + logAudio(`[play] [start] ${id}`); + clip.play().then((x) => { + logAudio(`[play] [ end ] ${id}`); }); + }); - clip.buffer().then(async () => { - logAudio(`[load] [ end ] ${id}`); - }); + clip.buffer().then(async () => { + logAudio(`[load] [ end ] ${id}`); + }); - return () => { - clip.dispose(); - }; - }, - end: (x: ITimelineEvent<'end', null>) => { - logEnd('Session finished'); - const $finishTraining = document.querySelector('.finish-training') as HTMLDivElement | null; - if ($finishTraining) { - $finishTraining.click(); - } - }, - theme: (x: ITimelineEvent<'theme', string>) => { - logTheme(x.detail); - THEME_ID.value = x.detail; - }, - lowRpm: (x: ITimelineEvent<'lowRpm', number>) => { - logLowRpm(x.detail); - const d = DIFFICULTY.value; - LOW_LIMIT.value = x.detail + ((d > 0) ? (d * (-1/120 * d + 5 / 2)) : d); - }, - debugAlert: (x: ITimelineEvent<'debugAlert', string>) => { - alert(x.detail); - }, + return () => { + clip.dispose(); + }; + }, + end: (x: ITimelineEvent<'end', null>) => { + logEnd('Session finished'); + const $finishTraining = document.querySelector( + '.finish-training', + ) as HTMLDivElement | null; + if ($finishTraining) { + $finishTraining.click(); + } + }, + theme: (x: ITimelineEvent<'theme', string>) => { + logTheme(x.detail); + THEME_ID.value = x.detail; }, -); + lowRpm: (x: ITimelineEvent<'lowRpm', number>) => { + logLowRpm(x.detail); + const d = DIFFICULTY.value; + LOW_LIMIT.value = x.detail + (d > 0 ? d * ((-1 / 120) * d + 5 / 2) : d); + }, + debugAlert: (x: ITimelineEvent<'debugAlert', string>) => { + alert(x.detail); + }, +}); // @ts-ignore window.nextEvent = timeLine.nextEvent; export const StoryManager = () => { - ROUTER_ID.subscribe(() => { + ROUTER_ID.subscribe(() => { if (isStory()) { - const episode = Math.floor(Number.parseFloat(QUERY_PARAMETER.value.get('episode') ?? '0')); + const episode = Math.floor( + Number.parseFloat(QUERY_PARAMETER.value.get('episode') ?? '0'), + ); timeLine.storyId = episode; - + timeManager.addFn(timeLine.tick, FrameRateLevel.D0); } else { timeManager.removeFn(timeLine.tick); @@ -89,3 +93,39 @@ export const StoryManager = () => { } }); }; + +const convertTime = (totalMilliseconds: number) => { + const milliseconds = totalMilliseconds % 1000; + const totalSeconds = Math.floor(totalMilliseconds / 1000); + const seconds = totalSeconds % 60; + const totalMinutes = Math.floor(totalSeconds / 60); + const minutes = totalMinutes % 60; + const hours = Math.floor(totalMinutes / 60); + + return new Date(0, 0, 0, hours, minutes, seconds, milliseconds); +}; + +const convertStory = (x: ITimelineEvent[]): DataItem[] => x.map((x, index) => ({ + id: index, + content: x.label, + start: convertTime(x.time), + className: x.type, +})); + +export const visualizeStory = (index = 0) => { + const container = forceSelect('#story-visualize'); + + const items: DataItemCollectionType = convertStory(stories[index]); + + const options: TimelineOptions = { + height: '30vh', + timeAxis: { scale: 'minute', step: 2 }, + showMajorLabels: false, + }; + + // Create a Timeline + const timeline = new Timeline(container, items, options); +}; + +// @ts-ignore +window.visualizeStory = visualizeStory; diff --git a/src/stories/utils.ts b/src/stories/utils.ts index c7a17b7..785b9d2 100644 --- a/src/stories/utils.ts +++ b/src/stories/utils.ts @@ -14,6 +14,7 @@ export const AudioEvent = ( ): ITimelineEvent<"audio", IPlayAudioStoryEvent> => ({ time, type: "audio", + label: url, detail: { url, }, @@ -22,6 +23,7 @@ export const AudioEvent = ( export const EndEvent = (time: number): ITimelineEvent<"end", null> => ({ time, type: "end", + label: "end", detail: null, }); @@ -31,6 +33,7 @@ export const ThemeEvent = ( ): ITimelineEvent<"theme", string> => ({ time, type: "theme", + label: `theme: ${theme}`, detail: theme, }); @@ -40,6 +43,7 @@ export const LowRpmLimitEvent = ( ): ITimelineEvent<"lowRpm", number> => ({ time, type: "lowRpm", + label: `low-rpm: ${rpm}`, detail: rpm, }); @@ -48,6 +52,7 @@ export const DebugAlertEvent = ( text: string ): ITimelineEvent<"debugAlert", string> => ({ time, + label: `alert: ${text}`, type: "debugAlert", detail: text, }); diff --git a/src/ResourceTracker.ts b/src/utils/ResourceTracker.ts similarity index 89% rename from src/ResourceTracker.ts rename to src/utils/ResourceTracker.ts index f1d9573..8207e65 100644 --- a/src/ResourceTracker.ts +++ b/src/utils/ResourceTracker.ts @@ -1,3 +1,4 @@ +// biome-ignore lint/suspicious/noExplicitAny: This is safe type Any = any; interface IDisposable extends Any { diff --git a/src/utils/TimeLine.ts b/src/utils/TimeLine.ts index 047ba1e..06887d9 100644 --- a/src/utils/TimeLine.ts +++ b/src/utils/TimeLine.ts @@ -1,6 +1,7 @@ export interface ITimelineEvent { time: number; type: Type; + label: string; detail: Detail; } diff --git a/yarn.lock b/yarn.lock index bcf1ea2..01e729a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2760,6 +2760,13 @@ __metadata: languageName: node linkType: hard +"moment@npm:^2.30.1": + version: 2.30.1 + resolution: "moment@npm:2.30.1" + checksum: 10/ae42d876d4ec831ef66110bdc302c0657c664991e45cf2afffc4b0f6cd6d251dde11375c982a5c0564ccc0fa593fc564576ddceb8c8845e87c15f58aa6baca69 + languageName: node + linkType: hard + "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" @@ -3504,6 +3511,7 @@ __metadata: "@web-media/event-target": "npm:^0.0.2" "@web-media/phonograph": "npm:2.3.2" debug: "npm:^4.3.4" + moment: "npm:^2.30.1" parcel: "npm:2.12.0" parcel-reporter-static-files-copy: "npm:^1.5.3" process: "npm:^0.11.10" @@ -3512,6 +3520,8 @@ __metadata: three: "npm:0.138.3" tslib: "npm:^2.3.1" typescript: "npm:5.4.5" + vis-data: "npm:^7.1.9" + vis-timeline: "npm:^7.7.3" languageName: unknown linkType: soft @@ -3628,6 +3638,33 @@ __metadata: languageName: node linkType: hard +"vis-data@npm:^7.1.9": + version: 7.1.9 + resolution: "vis-data@npm:7.1.9" + peerDependencies: + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + vis-util: ^5.0.1 + checksum: 10/13cc6774cc225aa8a84d12c29d90188627fe8d937a93080113bd7b30d729fe8d6b2703322b41614ed48ad3f28f66e4090dfb1dce2ae7885a8938c2cffd7eebf3 + languageName: node + linkType: hard + +"vis-timeline@npm:^7.7.3": + version: 7.7.3 + resolution: "vis-timeline@npm:7.7.3" + peerDependencies: + "@egjs/hammerjs": ^2.0.0 + component-emitter: ^1.3.0 + keycharm: ^0.2.0 || ^0.3.0 || ^0.4.0 + moment: ^2.24.0 + propagating-hammerjs: ^1.4.0 || ^2.0.0 + uuid: ^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + vis-data: ^6.3.0 || ^7.0.0 + vis-util: ^5.0.1 + xss: ^1.0.0 + checksum: 10/00aec604e63cc2941e9eb19eb25a4ff2bfb26715858f2213e39bc58c9d6b113e7aeb9bd2ab2b8f5918f1458fc47c4cf7bb0faa44765504a9651f6cd353117573 + languageName: node + linkType: hard + "weak-lru-cache@npm:^1.2.2": version: 1.2.2 resolution: "weak-lru-cache@npm:1.2.2"