anxuanng commited on
Commit
b2c4d34
·
verified ·
1 Parent(s): ec4da3b

Viewer for codepawl/100style-glb

Browse files
Files changed (2) hide show
  1. README.md +5 -5
  2. index.html +232 -17
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
- title: 100style Viewer
3
- emoji: 🚀
4
- colorFrom: purple
5
- colorTo: pink
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: 100STYLE locomotion as GLB viewer
3
+ emoji: 📦
4
+ colorFrom: gray
5
+ colorTo: gray
6
  sdk: static
7
  pinned: false
8
  ---
9
 
10
+ Browse https://huggingface.co/datasets/codepawl/100style-glb in the browser.
index.html CHANGED
@@ -1,19 +1,234 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>Motion pack viewer</title>
7
+ <style>
8
+ :root { --background: #ecece9; --panel: #f7f7f5; --text: #1d1d1b; --muted: #6b6b66; --fill: #e2e2de; --accent: #2f5bd3; --ground: #d9d9d4; }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root { --background: #161615; --panel: #1f1f1d; --text: #ececea; --muted: #9a9a94; --fill: #2b2b28; --accent: #8aa7ff; --ground: #232321; }
11
+ }
12
+ * { box-sizing: border-box; }
13
+ html, body { height: 100%; }
14
+ body { margin: 0; font: 15px/1.4 system-ui, sans-serif; background: var(--background); color: var(--text); display: grid; grid-template-columns: 300px 1fr; }
15
+ aside { background: var(--panel); overflow: auto; padding: 12px; user-select: none; display: flex; flex-direction: column; gap: 10px; }
16
+ input { width: 100%; padding: 10px 12px; border-radius: 10px; border: 0; background: var(--fill); color: var(--text); font: inherit; }
17
+ .chips { display: flex; flex-wrap: wrap; gap: 4px; }
18
+ .chips button { border: 0; border-radius: 8px; padding: 7px 10px; background: none; color: var(--muted); font: inherit; cursor: pointer; }
19
+ .chips button[aria-pressed="true"] { background: var(--fill); color: var(--text); }
20
+ .chips button:disabled { opacity: 0.35; cursor: default; }
21
+ #styles { display: flex; flex-direction: column; gap: 2px; }
22
+ #styles button { display: block; width: 100%; padding: 8px 10px; border: 0; border-radius: 10px; background: none; color: inherit; text-align: left; cursor: pointer; font: inherit; }
23
+ #styles button:hover, #styles button[aria-current="true"] { background: var(--fill); }
24
+ #styles small { display: block; color: var(--muted); font-size: 13px; }
25
+ main { position: relative; min-height: 0; }
26
+ canvas { width: 100%; height: 100%; display: block; touch-action: none; }
27
+ #status { position: absolute; left: 16px; top: 14px; display: flex; align-items: center; gap: 8px; color: var(--muted); }
28
+ #status svg { width: 16px; height: 16px; flex: none; }
29
+ #status[data-state="loading"] svg { animation: spin 0.9s linear infinite; }
30
+ #status[data-state="failed"] { color: #c2410c; }
31
+ @keyframes spin { to { transform: rotate(360deg); } }
32
+ @media (max-width: 760px) {
33
+ body { grid-template-columns: 1fr; grid-template-rows: 58vh 1fr; }
34
+ aside { order: 2; padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); }
35
+ }
36
+ </style>
37
+ <script type="importmap">
38
+ { "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.187.0/build/three.module.js", "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.187.0/examples/jsm/" } }
39
+ </script>
40
+ </head>
41
+ <body>
42
+ <aside>
43
+ <div class="chips" id="tiers" role="group" aria-label="File"></div>
44
+ <div class="chips" id="movements" role="group" aria-label="Movement"></div>
45
+ <input id="search" type="search" placeholder="Search styles" aria-label="Search styles">
46
+ <div id="styles"></div>
47
+ </aside>
48
+ <main>
49
+ <canvas id="view"></canvas>
50
+ <div id="status" data-state="loading" aria-live="polite"><svg viewBox="0 0 16 16" aria-hidden="true"></svg><span></span></div>
51
+ </main>
52
+ <script type="module">
53
+ // Motion pack viewer: pick a style and a movement, the clip plays on the character (or the bare source skeleton) and
54
+ // the camera follows the root. Reads metadata.jsonl from the dataset; ?base=<url> points it at a local copy.
55
+ import * as THREE from 'three';
56
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
57
+ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
58
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
59
+ import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
60
+ import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
61
+
62
+ const PACK = {"repository":"codepawl/100style-glb","title":"100STYLE locomotion as GLB"};
63
+ const base = new URLSearchParams(location.search).get('base') ?? `https://huggingface.co/datasets/${PACK.repository}/resolve/main`;
64
+ document.title = PACK.title ?? document.title;
65
+ const isPhone = matchMedia('(max-width: 760px)').matches;
66
+ const TIERS = [
67
+ { key: isPhone ? 'mobile' : 'web', label: 'Character' },
68
+ { key: 'skeleton', label: 'Skeleton' },
69
+ ];
70
+ const STATUS_ICONS = {
71
+ loading: '<circle cx="8" cy="8" r="6" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="26 12"/>',
72
+ ready: '<path d="M3 8.5l3 3 7-7" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>',
73
+ failed: '<path d="M8 1.5l7 13H1z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M8 6v4M8 12v.5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/>',
74
+ };
75
+
76
+ const statusElement = document.getElementById('status');
77
+ function setStatus(state, text) {
78
+ statusElement.dataset.state = state;
79
+ statusElement.querySelector('svg').innerHTML = STATUS_ICONS[state];
80
+ statusElement.querySelector('span').textContent = text;
81
+ }
82
+
83
+ const canvas = document.getElementById('view');
84
+ const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
85
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
86
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
87
+ const style = getComputedStyle(document.documentElement);
88
+ const scene = new THREE.Scene();
89
+ scene.background = new THREE.Color(style.getPropertyValue('--background').trim());
90
+ scene.fog = new THREE.Fog(scene.background, 12, 40);
91
+ scene.environment = new THREE.PMREMGenerator(renderer).fromScene(new RoomEnvironment(), 0.04).texture;
92
+ scene.environmentIntensity = 0.7;
93
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x8a8a84, 1.4));
94
+ const sun = new THREE.DirectionalLight(0xffffff, 1.6);
95
+ sun.position.set(3, 6, 4);
96
+ scene.add(sun);
97
+ const ground = new THREE.GridHelper(400, 400, style.getPropertyValue('--muted').trim(), style.getPropertyValue('--ground').trim());
98
+ ground.material.transparent = true;
99
+ ground.material.opacity = 0.55;
100
+ scene.add(ground);
101
+ const camera = new THREE.PerspectiveCamera(35, 1, 0.05, 200);
102
+ camera.position.set(2.6, 1.6, 3.6);
103
+ const controls = new OrbitControls(camera, canvas);
104
+ controls.target.set(0, 0.9, 0);
105
+ controls.enablePan = false;
106
+ controls.minDistance = 1.2;
107
+ controls.maxDistance = 14;
108
+ const loader = new GLTFLoader()
109
+ .setMeshoptDecoder(MeshoptDecoder)
110
+ .setKTX2Loader(new KTX2Loader().setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.187.0/examples/jsm/libs/basis/').detectSupport(renderer));
111
+
112
+ let rows = [];
113
+ let current = null;
114
+ let mixer = null;
115
+ let followBone = null;
116
+ let loadToken = 0;
117
+ const selection = { style: null, movement: 'FW', tier: TIERS[0].key };
118
+ const clock = new THREE.Clock();
119
+
120
+ function resize() {
121
+ const { clientWidth, clientHeight } = canvas.parentElement;
122
+ renderer.setSize(clientWidth, clientHeight, false);
123
+ camera.aspect = clientWidth / clientHeight;
124
+ camera.updateProjectionMatrix();
125
+ }
126
+ new ResizeObserver(resize).observe(canvas.parentElement);
127
+
128
+ function rowFor(styleName, movement) {
129
+ return rows.find((row) => row.style === styleName && row.movement === movement);
130
+ }
131
+
132
+ async function play() {
133
+ const row = rowFor(selection.style, selection.movement) ?? rows.find((candidate) => candidate.style === selection.style);
134
+ if (!row) return;
135
+ selection.movement = row.movement;
136
+ renderChips();
137
+ history.replaceState(null, '', `#${row.id}`);
138
+ const relative = row.files[selection.tier] ?? row.files.web;
139
+ const token = ++loadToken;
140
+ setStatus('loading', `${row.title}`);
141
+ try {
142
+ const gltf = await loader.loadAsync(`${base}/${relative}`);
143
+ if (token !== loadToken) return;
144
+ if (current) scene.remove(current);
145
+ current = gltf.scene;
146
+ scene.add(current);
147
+ let skeletonOnly = true;
148
+ const bones = [];
149
+ current.traverse((object) => {
150
+ if (object.isMesh) skeletonOnly = false;
151
+ if (object.isBone) bones.push(object);
152
+ });
153
+ if (skeletonOnly) {
154
+ const helper = new THREE.SkeletonHelper(current);
155
+ helper.material.linewidth = 2;
156
+ current.add(helper);
157
+ for (const bone of bones) bone.add(new THREE.Mesh(new THREE.SphereGeometry(0.022, 10, 8), new THREE.MeshStandardMaterial({ color: style.getPropertyValue('--accent').trim() })));
158
+ }
159
+ followBone = bones.find((bone) => /pelvis|hips/i.test(bone.name)) ?? bones[0] ?? current;
160
+ mixer = new THREE.AnimationMixer(current);
161
+ mixer.clipAction(gltf.animations[0]).play();
162
+ setStatus('ready', `${row.title} · ${row.seconds?.toFixed(0) ?? '?'} s · ${((row[`${selection.tier}_bytes`] ?? 0) / 1e6).toFixed(1)} MB`);
163
+ } catch (error) {
164
+ if (token === loadToken) setStatus('failed', `Could not load ${relative}`);
165
+ }
166
+ }
167
+
168
+ function renderChips() {
169
+ const tiers = document.getElementById('tiers');
170
+ tiers.replaceChildren(...TIERS.map(({ key, label }) => {
171
+ const button = document.createElement('button');
172
+ button.textContent = label;
173
+ button.setAttribute('aria-pressed', String(selection.tier === key));
174
+ button.onclick = () => { selection.tier = key; play(); };
175
+ return button;
176
+ }));
177
+ const movements = [...new Map(rows.map((row) => [row.movement, row.movement_name])).entries()];
178
+ const available = new Set(rows.filter((row) => row.style === selection.style).map((row) => row.movement));
179
+ document.getElementById('movements').replaceChildren(...movements.map(([code, name]) => {
180
+ const button = document.createElement('button');
181
+ button.textContent = name ?? code;
182
+ button.disabled = !available.has(code);
183
+ button.setAttribute('aria-pressed', String(selection.movement === code));
184
+ button.onclick = () => { selection.movement = code; play(); };
185
+ return button;
186
+ }));
187
+ }
188
+
189
+ function renderStyles() {
190
+ const query = document.getElementById('search').value.toLowerCase();
191
+ const styles = [...new Map(rows.map((row) => [row.style, row.description])).entries()]
192
+ .filter(([name, description]) => `${name} ${description ?? ''}`.toLowerCase().includes(query));
193
+ document.getElementById('styles').replaceChildren(...styles.map(([name, description]) => {
194
+ const button = document.createElement('button');
195
+ button.setAttribute('aria-current', String(selection.style === name));
196
+ button.innerHTML = '<span></span><small></small>';
197
+ button.querySelector('span').textContent = name.replace(/([a-z])([A-Z])/g, '$1 $2');
198
+ button.querySelector('small').textContent = description ?? '';
199
+ button.onclick = () => {
200
+ selection.style = name;
201
+ renderStyles();
202
+ play();
203
+ };
204
+ return button;
205
+ }));
206
+ }
207
+
208
+ const text = await (await fetch(`${base}/metadata.jsonl`)).text();
209
+ rows = text.split('\n').filter(Boolean).map((line) => JSON.parse(line)).filter((row) => row.style);
210
+ const fromHash = rows.find((row) => row.id === location.hash.slice(1));
211
+ selection.style = fromHash?.style ?? rows.find((row) => row.style === 'Neutral')?.style ?? rows[0]?.style;
212
+ selection.movement = fromHash?.movement ?? 'FW';
213
+ document.getElementById('search').oninput = renderStyles;
214
+ renderStyles();
215
+ play();
216
+
217
+ const followed = new THREE.Vector3();
218
+ renderer.setAnimationLoop(() => {
219
+ const delta = Math.min(clock.getDelta(), 0.1);
220
+ mixer?.update(delta);
221
+ if (followBone) {
222
+ followBone.getWorldPosition(followed);
223
+ // Follow the root along the ground; the orbit keeps its angle and distance.
224
+ const shift = new THREE.Vector3(followed.x - controls.target.x, 0, followed.z - controls.target.z);
225
+ controls.target.add(shift);
226
+ camera.position.add(shift);
227
+ controls.target.y += (followed.y * 0.9 - controls.target.y) * 0.1;
228
+ }
229
+ controls.update();
230
+ renderer.render(scene, camera);
231
+ });
232
+ </script>
233
+ </body>
234
  </html>