sumulige-claude 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,683 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>ApexOS - 3D Power Analysis</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'JetBrains Mono', 'SF Mono', monospace;
16
+ background: #0a0a0a;
17
+ color: #fff;
18
+ overflow: hidden;
19
+ }
20
+
21
+ #container {
22
+ width: 100vw;
23
+ height: 100vh;
24
+ position: relative;
25
+ }
26
+
27
+ #info {
28
+ position: absolute;
29
+ top: 20px;
30
+ left: 20px;
31
+ z-index: 100;
32
+ background: rgba(0, 0, 0, 0.8);
33
+ padding: 20px;
34
+ border-radius: 12px;
35
+ border: 1px solid rgba(255, 255, 255, 0.1);
36
+ backdrop-filter: blur(10px);
37
+ max-width: 320px;
38
+ }
39
+
40
+ #info h1 {
41
+ font-size: 18px;
42
+ font-weight: 600;
43
+ margin-bottom: 8px;
44
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+
50
+ #info p {
51
+ font-size: 12px;
52
+ color: #888;
53
+ line-height: 1.6;
54
+ margin-bottom: 16px;
55
+ }
56
+
57
+ .legend {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 8px;
61
+ }
62
+
63
+ .legend-item {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 10px;
67
+ font-size: 12px;
68
+ }
69
+
70
+ .legend-color {
71
+ width: 12px;
72
+ height: 12px;
73
+ border-radius: 50%;
74
+ }
75
+
76
+ .legend-label {
77
+ color: #aaa;
78
+ }
79
+
80
+ .legend-value {
81
+ margin-left: auto;
82
+ font-variant-numeric: tabular-nums;
83
+ color: #fff;
84
+ }
85
+
86
+ #stats {
87
+ position: absolute;
88
+ top: 20px;
89
+ right: 20px;
90
+ z-index: 100;
91
+ background: rgba(0, 0, 0, 0.8);
92
+ padding: 16px 20px;
93
+ border-radius: 12px;
94
+ border: 1px solid rgba(255, 255, 255, 0.1);
95
+ backdrop-filter: blur(10px);
96
+ }
97
+
98
+ .stat-row {
99
+ display: flex;
100
+ justify-content: space-between;
101
+ gap: 40px;
102
+ margin-bottom: 8px;
103
+ }
104
+
105
+ .stat-row:last-child {
106
+ margin-bottom: 0;
107
+ }
108
+
109
+ .stat-label {
110
+ font-size: 11px;
111
+ color: #666;
112
+ text-transform: uppercase;
113
+ letter-spacing: 0.5px;
114
+ }
115
+
116
+ .stat-value {
117
+ font-size: 14px;
118
+ font-weight: 600;
119
+ font-variant-numeric: tabular-nums;
120
+ }
121
+
122
+ .stat-value.power { color: #667eea; }
123
+ .stat-value.hr { color: #f56565; }
124
+ .stat-value.time { color: #48bb78; }
125
+
126
+ #tooltip {
127
+ position: absolute;
128
+ background: rgba(0, 0, 0, 0.9);
129
+ padding: 12px 16px;
130
+ border-radius: 8px;
131
+ border: 1px solid rgba(255, 255, 255, 0.2);
132
+ font-size: 12px;
133
+ pointer-events: none;
134
+ opacity: 0;
135
+ transition: opacity 0.2s;
136
+ z-index: 200;
137
+ }
138
+
139
+ #tooltip.visible {
140
+ opacity: 1;
141
+ }
142
+
143
+ #controls {
144
+ position: absolute;
145
+ bottom: 20px;
146
+ left: 50%;
147
+ transform: translateX(-50%);
148
+ z-index: 100;
149
+ display: flex;
150
+ gap: 12px;
151
+ }
152
+
153
+ .control-btn {
154
+ background: rgba(255, 255, 255, 0.1);
155
+ border: 1px solid rgba(255, 255, 255, 0.2);
156
+ color: #fff;
157
+ padding: 10px 20px;
158
+ border-radius: 8px;
159
+ cursor: pointer;
160
+ font-size: 12px;
161
+ font-family: inherit;
162
+ transition: all 0.2s;
163
+ }
164
+
165
+ .control-btn:hover {
166
+ background: rgba(255, 255, 255, 0.2);
167
+ border-color: rgba(255, 255, 255, 0.3);
168
+ }
169
+
170
+ .control-btn.active {
171
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
172
+ border-color: transparent;
173
+ }
174
+
175
+ /* Axis labels */
176
+ .axis-label {
177
+ position: absolute;
178
+ font-size: 11px;
179
+ color: #666;
180
+ text-transform: uppercase;
181
+ letter-spacing: 1px;
182
+ }
183
+
184
+ #x-label { bottom: 60px; left: 50%; transform: translateX(-50%); }
185
+ #y-label { left: 20px; top: 50%; transform: rotate(-90deg) translateX(-50%); transform-origin: left center; }
186
+ #z-label { right: 60px; bottom: 50%; }
187
+ </style>
188
+ </head>
189
+ <body>
190
+ <div id="container"></div>
191
+
192
+ <div id="info">
193
+ <h1>3D Power Analysis</h1>
194
+ <p>Visualizing the relationship between Power, Time, and Heart Rate during your ride.</p>
195
+ <div class="legend">
196
+ <div class="legend-item">
197
+ <div class="legend-color" style="background: #667eea;"></div>
198
+ <span class="legend-label">X-Axis: Time</span>
199
+ <span class="legend-value">0-60 min</span>
200
+ </div>
201
+ <div class="legend-item">
202
+ <div class="legend-color" style="background: #48bb78;"></div>
203
+ <span class="legend-label">Y-Axis: Power</span>
204
+ <span class="legend-value">0-400 W</span>
205
+ </div>
206
+ <div class="legend-item">
207
+ <div class="legend-color" style="background: #f56565;"></div>
208
+ <span class="legend-label">Z-Axis: Heart Rate</span>
209
+ <span class="legend-value">100-180 bpm</span>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div id="stats">
215
+ <div class="stat-row">
216
+ <span class="stat-label">Avg Power</span>
217
+ <span class="stat-value power" id="avg-power">--</span>
218
+ </div>
219
+ <div class="stat-row">
220
+ <span class="stat-label">Avg HR</span>
221
+ <span class="stat-value hr" id="avg-hr">--</span>
222
+ </div>
223
+ <div class="stat-row">
224
+ <span class="stat-label">Duration</span>
225
+ <span class="stat-value time" id="duration">--</span>
226
+ </div>
227
+ <div class="stat-row">
228
+ <span class="stat-label">Data Points</span>
229
+ <span class="stat-value" id="points">--</span>
230
+ </div>
231
+ </div>
232
+
233
+ <div id="tooltip"></div>
234
+
235
+ <div id="controls">
236
+ <button class="control-btn active" data-view="perspective">Perspective</button>
237
+ <button class="control-btn" data-view="top">Top (Time-Power)</button>
238
+ <button class="control-btn" data-view="front">Front (Power-HR)</button>
239
+ <button class="control-btn" data-view="side">Side (Time-HR)</button>
240
+ <button class="control-btn" data-action="animate">Auto Rotate</button>
241
+ </div>
242
+
243
+ <script type="importmap">
244
+ {
245
+ "imports": {
246
+ "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
247
+ "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
248
+ }
249
+ }
250
+ </script>
251
+
252
+ <script type="module">
253
+ import * as THREE from 'three';
254
+ import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
255
+
256
+ // ============ Configuration ============
257
+ const CONFIG = {
258
+ pointCount: 600, // 10 points per minute for 60 min
259
+ pointSize: 0.08,
260
+ axisSize: 8,
261
+ gridDivisions: 10,
262
+ colors: {
263
+ power: 0x667eea, // Purple-blue
264
+ hr: 0xf56565, // Red
265
+ time: 0x48bb78, // Green
266
+ grid: 0x333333,
267
+ background: 0x0a0a0a
268
+ }
269
+ };
270
+
271
+ // ============ Generate Cycling Data ============
272
+ function generateCyclingData(pointCount) {
273
+ const data = [];
274
+ const duration = 60; // 60 minutes
275
+
276
+ // Simulate a structured workout:
277
+ // 0-10 min: Warmup
278
+ // 10-20 min: Intervals (high power)
279
+ // 20-35 min: Steady state
280
+ // 35-50 min: More intervals
281
+ // 50-60 min: Cooldown
282
+
283
+ for (let i = 0; i < pointCount; i++) {
284
+ const t = (i / pointCount) * duration;
285
+
286
+ let basePower, baseHR;
287
+
288
+ if (t < 10) {
289
+ // Warmup: gradually increasing
290
+ basePower = 150 + (t / 10) * 50;
291
+ baseHR = 110 + (t / 10) * 20;
292
+ } else if (t < 20) {
293
+ // Intervals: high power spikes
294
+ const intervalPhase = ((t - 10) % 2) / 2;
295
+ basePower = intervalPhase < 0.5 ? 320 : 180;
296
+ baseHR = intervalPhase < 0.5 ? 165 : 140;
297
+ } else if (t < 35) {
298
+ // Steady state
299
+ basePower = 240;
300
+ baseHR = 150;
301
+ } else if (t < 50) {
302
+ // More intense intervals
303
+ const intervalPhase = ((t - 35) % 1.5) / 1.5;
304
+ basePower = intervalPhase < 0.6 ? 350 : 200;
305
+ baseHR = intervalPhase < 0.6 ? 175 : 145;
306
+ } else {
307
+ // Cooldown
308
+ const cooldownProgress = (t - 50) / 10;
309
+ basePower = 200 - cooldownProgress * 80;
310
+ baseHR = 145 - cooldownProgress * 30;
311
+ }
312
+
313
+ // Add realistic noise
314
+ const noise = () => (Math.random() - 0.5) * 2;
315
+ const power = Math.max(50, basePower + noise() * 30);
316
+ const hr = Math.max(90, Math.min(190, baseHR + noise() * 8));
317
+
318
+ data.push({
319
+ time: t,
320
+ power: power,
321
+ hr: hr,
322
+ // Calculate intensity zone for coloring
323
+ intensity: power > 300 ? 'high' : power > 220 ? 'medium' : 'low'
324
+ });
325
+ }
326
+
327
+ return data;
328
+ }
329
+
330
+ // ============ Scene Setup ============
331
+ const container = document.getElementById('container');
332
+ const scene = new THREE.Scene();
333
+ scene.background = new THREE.Color(CONFIG.colors.background);
334
+
335
+ const camera = new THREE.PerspectiveCamera(
336
+ 60,
337
+ window.innerWidth / window.innerHeight,
338
+ 0.1,
339
+ 1000
340
+ );
341
+ camera.position.set(12, 10, 12);
342
+
343
+ const renderer = new THREE.WebGLRenderer({
344
+ antialias: true,
345
+ alpha: true
346
+ });
347
+ renderer.setSize(window.innerWidth, window.innerHeight);
348
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
349
+ container.appendChild(renderer.domElement);
350
+
351
+ // Controls
352
+ const controls = new OrbitControls(camera, renderer.domElement);
353
+ controls.enableDamping = true;
354
+ controls.dampingFactor = 0.05;
355
+ controls.minDistance = 5;
356
+ controls.maxDistance = 30;
357
+
358
+ // ============ Create Axes ============
359
+ function createAxes() {
360
+ const axisGroup = new THREE.Group();
361
+ const size = CONFIG.axisSize;
362
+
363
+ // Axis lines with glow effect
364
+ const axisMaterial = (color) => new THREE.LineBasicMaterial({
365
+ color,
366
+ linewidth: 2,
367
+ transparent: true,
368
+ opacity: 0.8
369
+ });
370
+
371
+ // X-axis (Time) - Blue-purple
372
+ const xGeom = new THREE.BufferGeometry().setFromPoints([
373
+ new THREE.Vector3(0, 0, 0),
374
+ new THREE.Vector3(size, 0, 0)
375
+ ]);
376
+ axisGroup.add(new THREE.Line(xGeom, axisMaterial(CONFIG.colors.power)));
377
+
378
+ // Y-axis (Power) - Green
379
+ const yGeom = new THREE.BufferGeometry().setFromPoints([
380
+ new THREE.Vector3(0, 0, 0),
381
+ new THREE.Vector3(0, size, 0)
382
+ ]);
383
+ axisGroup.add(new THREE.Line(yGeom, axisMaterial(CONFIG.colors.time)));
384
+
385
+ // Z-axis (HR) - Red
386
+ const zGeom = new THREE.BufferGeometry().setFromPoints([
387
+ new THREE.Vector3(0, 0, 0),
388
+ new THREE.Vector3(0, 0, size)
389
+ ]);
390
+ axisGroup.add(new THREE.Line(zGeom, axisMaterial(CONFIG.colors.hr)));
391
+
392
+ // Grid on XZ plane (bottom)
393
+ const gridXZ = new THREE.GridHelper(size, CONFIG.gridDivisions, CONFIG.colors.grid, CONFIG.colors.grid);
394
+ gridXZ.position.set(size/2, 0, size/2);
395
+ gridXZ.material.transparent = true;
396
+ gridXZ.material.opacity = 0.3;
397
+ axisGroup.add(gridXZ);
398
+
399
+ // Grid on XY plane (back)
400
+ const gridXY = new THREE.GridHelper(size, CONFIG.gridDivisions, CONFIG.colors.grid, CONFIG.colors.grid);
401
+ gridXY.rotation.x = Math.PI / 2;
402
+ gridXY.position.set(size/2, size/2, 0);
403
+ gridXY.material.transparent = true;
404
+ gridXY.material.opacity = 0.2;
405
+ axisGroup.add(gridXY);
406
+
407
+ // Grid on YZ plane (left)
408
+ const gridYZ = new THREE.GridHelper(size, CONFIG.gridDivisions, CONFIG.colors.grid, CONFIG.colors.grid);
409
+ gridYZ.rotation.z = Math.PI / 2;
410
+ gridYZ.position.set(0, size/2, size/2);
411
+ gridYZ.material.transparent = true;
412
+ gridYZ.material.opacity = 0.2;
413
+ axisGroup.add(gridYZ);
414
+
415
+ return axisGroup;
416
+ }
417
+
418
+ // ============ Create Data Points ============
419
+ function createScatterPoints(data) {
420
+ const geometry = new THREE.BufferGeometry();
421
+ const positions = [];
422
+ const colors = [];
423
+ const sizes = [];
424
+
425
+ const size = CONFIG.axisSize;
426
+ const color = new THREE.Color();
427
+
428
+ // Normalize data ranges
429
+ const timeRange = { min: 0, max: 60 };
430
+ const powerRange = { min: 50, max: 400 };
431
+ const hrRange = { min: 100, max: 180 };
432
+
433
+ data.forEach((point, i) => {
434
+ // Map to 3D coordinates
435
+ const x = THREE.MathUtils.mapLinear(point.time, timeRange.min, timeRange.max, 0, size);
436
+ const y = THREE.MathUtils.mapLinear(point.power, powerRange.min, powerRange.max, 0, size);
437
+ const z = THREE.MathUtils.mapLinear(point.hr, hrRange.min, hrRange.max, 0, size);
438
+
439
+ positions.push(x, y, z);
440
+
441
+ // Color based on power intensity
442
+ const powerNorm = (point.power - powerRange.min) / (powerRange.max - powerRange.min);
443
+ color.setHSL(
444
+ 0.7 - powerNorm * 0.5, // Hue: blue to red
445
+ 0.8,
446
+ 0.5 + powerNorm * 0.2
447
+ );
448
+ colors.push(color.r, color.g, color.b);
449
+
450
+ // Size based on HR (higher HR = larger point)
451
+ const hrNorm = (point.hr - hrRange.min) / (hrRange.max - hrRange.min);
452
+ sizes.push(CONFIG.pointSize * (0.8 + hrNorm * 0.4));
453
+ });
454
+
455
+ geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
456
+ geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
457
+ geometry.setAttribute('size', new THREE.Float32BufferAttribute(sizes, 1));
458
+
459
+ // Custom shader material for better looking points
460
+ const material = new THREE.ShaderMaterial({
461
+ uniforms: {
462
+ uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }
463
+ },
464
+ vertexShader: `
465
+ attribute float size;
466
+ attribute vec3 color;
467
+ varying vec3 vColor;
468
+ uniform float uPixelRatio;
469
+
470
+ void main() {
471
+ vColor = color;
472
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
473
+ gl_PointSize = size * uPixelRatio * (300.0 / -mvPosition.z);
474
+ gl_Position = projectionMatrix * mvPosition;
475
+ }
476
+ `,
477
+ fragmentShader: `
478
+ varying vec3 vColor;
479
+
480
+ void main() {
481
+ float dist = length(gl_PointCoord - vec2(0.5));
482
+ if (dist > 0.5) discard;
483
+
484
+ // Soft edge with glow
485
+ float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
486
+ float glow = exp(-dist * 4.0) * 0.5;
487
+
488
+ gl_FragColor = vec4(vColor + glow, alpha);
489
+ }
490
+ `,
491
+ transparent: true,
492
+ depthWrite: false,
493
+ blending: THREE.AdditiveBlending
494
+ });
495
+
496
+ return new THREE.Points(geometry, material);
497
+ }
498
+
499
+ // ============ Create Trail Lines ============
500
+ function createTrailLines(data) {
501
+ const size = CONFIG.axisSize;
502
+ const timeRange = { min: 0, max: 60 };
503
+ const powerRange = { min: 50, max: 400 };
504
+ const hrRange = { min: 100, max: 180 };
505
+
506
+ const points = data.map(point => new THREE.Vector3(
507
+ THREE.MathUtils.mapLinear(point.time, timeRange.min, timeRange.max, 0, size),
508
+ THREE.MathUtils.mapLinear(point.power, powerRange.min, powerRange.max, 0, size),
509
+ THREE.MathUtils.mapLinear(point.hr, hrRange.min, hrRange.max, 0, size)
510
+ ));
511
+
512
+ const geometry = new THREE.BufferGeometry().setFromPoints(points);
513
+ const material = new THREE.LineBasicMaterial({
514
+ color: 0x667eea,
515
+ transparent: true,
516
+ opacity: 0.3,
517
+ linewidth: 1
518
+ });
519
+
520
+ return new THREE.Line(geometry, material);
521
+ }
522
+
523
+ // ============ Initialize Scene ============
524
+ const cyclingData = generateCyclingData(CONFIG.pointCount);
525
+
526
+ // Add elements to scene
527
+ const axes = createAxes();
528
+ scene.add(axes);
529
+
530
+ const scatterPoints = createScatterPoints(cyclingData);
531
+ scene.add(scatterPoints);
532
+
533
+ const trailLine = createTrailLines(cyclingData);
534
+ scene.add(trailLine);
535
+
536
+ // Ambient light
537
+ scene.add(new THREE.AmbientLight(0xffffff, 0.5));
538
+
539
+ // ============ Update Stats ============
540
+ function updateStats() {
541
+ const avgPower = cyclingData.reduce((sum, d) => sum + d.power, 0) / cyclingData.length;
542
+ const avgHR = cyclingData.reduce((sum, d) => sum + d.hr, 0) / cyclingData.length;
543
+ const duration = cyclingData[cyclingData.length - 1].time;
544
+
545
+ document.getElementById('avg-power').textContent = `${Math.round(avgPower)} W`;
546
+ document.getElementById('avg-hr').textContent = `${Math.round(avgHR)} bpm`;
547
+ document.getElementById('duration').textContent = `${Math.round(duration)} min`;
548
+ document.getElementById('points').textContent = cyclingData.length.toLocaleString();
549
+ }
550
+ updateStats();
551
+
552
+ // ============ View Controls ============
553
+ const viewPositions = {
554
+ perspective: { pos: [12, 10, 12], target: [4, 4, 4] },
555
+ top: { pos: [4, 15, 4], target: [4, 0, 4] },
556
+ front: { pos: [4, 4, 15], target: [4, 4, 0] },
557
+ side: { pos: [15, 4, 4], target: [0, 4, 4] }
558
+ };
559
+
560
+ let isAutoRotating = false;
561
+
562
+ document.querySelectorAll('.control-btn').forEach(btn => {
563
+ btn.addEventListener('click', () => {
564
+ const view = btn.dataset.view;
565
+ const action = btn.dataset.action;
566
+
567
+ if (action === 'animate') {
568
+ isAutoRotating = !isAutoRotating;
569
+ controls.autoRotate = isAutoRotating;
570
+ controls.autoRotateSpeed = 1;
571
+ btn.classList.toggle('active', isAutoRotating);
572
+ return;
573
+ }
574
+
575
+ if (view && viewPositions[view]) {
576
+ const { pos, target } = viewPositions[view];
577
+
578
+ // Animate camera transition
579
+ const startPos = camera.position.clone();
580
+ const startTarget = controls.target.clone();
581
+ const endPos = new THREE.Vector3(...pos);
582
+ const endTarget = new THREE.Vector3(...target);
583
+
584
+ let t = 0;
585
+ const animateView = () => {
586
+ t += 0.05;
587
+ if (t >= 1) {
588
+ camera.position.copy(endPos);
589
+ controls.target.copy(endTarget);
590
+ return;
591
+ }
592
+
593
+ const easeT = 1 - Math.pow(1 - t, 3); // Ease out cubic
594
+ camera.position.lerpVectors(startPos, endPos, easeT);
595
+ controls.target.lerpVectors(startTarget, endTarget, easeT);
596
+ requestAnimationFrame(animateView);
597
+ };
598
+ animateView();
599
+
600
+ // Update button states
601
+ document.querySelectorAll('.control-btn[data-view]').forEach(b => b.classList.remove('active'));
602
+ btn.classList.add('active');
603
+ }
604
+ });
605
+ });
606
+
607
+ // ============ Animation Loop ============
608
+ const clock = new THREE.Clock();
609
+
610
+ function animate() {
611
+ requestAnimationFrame(animate);
612
+
613
+ const elapsed = clock.getElapsedTime();
614
+
615
+ // Subtle point animation
616
+ const positions = scatterPoints.geometry.attributes.position.array;
617
+ const sizes = scatterPoints.geometry.attributes.size.array;
618
+
619
+ for (let i = 0; i < sizes.length; i++) {
620
+ const baseSize = CONFIG.pointSize * (0.8 + Math.random() * 0.4);
621
+ sizes[i] = baseSize * (1 + Math.sin(elapsed * 2 + i * 0.1) * 0.1);
622
+ }
623
+ scatterPoints.geometry.attributes.size.needsUpdate = true;
624
+
625
+ controls.update();
626
+ renderer.render(scene, camera);
627
+ }
628
+ animate();
629
+
630
+ // ============ Resize Handler ============
631
+ window.addEventListener('resize', () => {
632
+ camera.aspect = window.innerWidth / window.innerHeight;
633
+ camera.updateProjectionMatrix();
634
+ renderer.setSize(window.innerWidth, window.innerHeight);
635
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
636
+ scatterPoints.material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
637
+ });
638
+
639
+ // ============ Raycaster for Hover ============
640
+ const raycaster = new THREE.Raycaster();
641
+ raycaster.params.Points.threshold = 0.2;
642
+ const mouse = new THREE.Vector2();
643
+ const tooltip = document.getElementById('tooltip');
644
+
645
+ window.addEventListener('mousemove', (event) => {
646
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
647
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
648
+
649
+ raycaster.setFromCamera(mouse, camera);
650
+ const intersects = raycaster.intersectObject(scatterPoints);
651
+
652
+ if (intersects.length > 0) {
653
+ const idx = intersects[0].index;
654
+ const point = cyclingData[idx];
655
+
656
+ tooltip.innerHTML = `
657
+ <div style="color: #667eea; font-weight: 600;">Point #${idx + 1}</div>
658
+ <div style="margin-top: 6px;">
659
+ <span style="color: #888;">Time:</span>
660
+ <span style="color: #48bb78;">${point.time.toFixed(1)} min</span>
661
+ </div>
662
+ <div>
663
+ <span style="color: #888;">Power:</span>
664
+ <span style="color: #667eea;">${Math.round(point.power)} W</span>
665
+ </div>
666
+ <div>
667
+ <span style="color: #888;">HR:</span>
668
+ <span style="color: #f56565;">${Math.round(point.hr)} bpm</span>
669
+ </div>
670
+ `;
671
+ tooltip.style.left = event.clientX + 15 + 'px';
672
+ tooltip.style.top = event.clientY + 15 + 'px';
673
+ tooltip.classList.add('visible');
674
+ } else {
675
+ tooltip.classList.remove('visible');
676
+ }
677
+ });
678
+
679
+ console.log('ApexOS 3D Power Analysis initialized');
680
+ console.log(`Rendered ${cyclingData.length} data points`);
681
+ </script>
682
+ </body>
683
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sumulige-claude",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "The Best Agent Harness for Claude Code",
5
5
  "main": "cli.js",
6
6
  "bin": {