Test: Difference between revisions
Jump to navigation
Jump to search
(Replaced content with "{{subst:Learning Log}}") Tag: Replaced |
No edit summary |
||
| Line 1: | Line 1: | ||
<html> | |||
<div id="ose-3d-mindmap" style="width:100%; height:700px; border:1px solid #ccc; overflow:hidden; position:relative;"></div> | |||
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script> | |||
<script src="https://unpkg.com/three@0.160.0/examples/js/controls/OrbitControls.js"></script> | |||
= | <script> | ||
(function () { | |||
const container = document.getElementById('ose-3d-mindmap'); | |||
if (!container) return; | |||
const words = [ | |||
"collaborative", | |||
"design", | |||
"for", | |||
"a", | |||
"transparent", | |||
"and", | |||
"inclusive", | |||
"economy", | |||
"of", | |||
"abundance" | |||
]; | |||
== | const scene = new THREE.Scene(); | ||
scene.background = new THREE.Color(0xffffff); | |||
const camera = new THREE.PerspectiveCamera( | |||
50, | |||
container.clientWidth / container.clientHeight, | |||
0.1, | |||
2000 | |||
); | |||
camera.position.set(0, 0, 260); | |||
= | const renderer = new THREE.WebGLRenderer({ antialias: true }); | ||
renderer.setPixelRatio(window.devicePixelRatio || 1); | |||
renderer.setSize(container.clientWidth, container.clientHeight); | |||
container.appendChild(renderer.domElement); | |||
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |||
controls.enableDamping = true; | |||
controls.dampingFactor = 0.08; | |||
controls.rotateSpeed = 0.8; | |||
controls.zoomSpeed = 0.9; | |||
controls.panSpeed = 0.7; | |||
= | const ambient = new THREE.AmbientLight(0xffffff, 1.2); | ||
scene.add(ambient); | |||
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); | |||
dirLight.position.set(120, 150, 200); | |||
scene.add(dirLight); | |||
= | function makeTextSprite(message) { | ||
''' | const canvas = document.createElement('canvas'); | ||
const ctx = canvas.getContext('2d'); | |||
''' | const fontSize = 48; | ||
const paddingX = 28; | |||
const paddingY = 18; | |||
ctx.font = 'bold ' + fontSize + 'px Arial'; | |||
const textWidth = Math.ceil(ctx.measureText(message).width); | |||
canvas.width = textWidth + paddingX * 2; | |||
canvas.height = fontSize + paddingY * 2; | |||
== | ctx.font = 'bold ' + fontSize + 'px Arial'; | ||
ctx.textBaseline = 'middle'; | |||
ctx.textAlign = 'center'; | |||
ctx.fillStyle = 'rgba(255,255,255,0.92)'; | |||
ctx.strokeStyle = 'rgba(40,40,40,0.9)'; | |||
ctx.lineWidth = 3; | |||
roundRect(ctx, 2, 2, canvas.width - 4, canvas.height - 4, 14, true, true); | |||
= | ctx.fillStyle = '#111111'; | ||
ctx.fillText(message, canvas.width / 2, canvas.height / 2); | |||
const texture = new THREE.CanvasTexture(canvas); | |||
texture.needsUpdate = true; | |||
= | const material = new THREE.SpriteMaterial({ | ||
map: texture, | |||
transparent: true | |||
}); | |||
const sprite = new THREE.Sprite(material); | |||
= | const scale = 0.34; | ||
sprite.scale.set(canvas.width * scale, canvas.height * scale, 1); | |||
{ | return sprite; | ||
} | |||
function roundRect(ctx, x, y, w, h, r, fill, stroke) { | |||
if (w < 2 * r) r = w / 2; | |||
if (h < 2 * r) r = h / 2; | |||
ctx.beginPath(); | |||
ctx.moveTo(x + r, y); | |||
ctx.arcTo(x + w, y, x + w, y + h, r); | |||
ctx.arcTo(x + w, y + h, x, y + h, r); | |||
ctx.arcTo(x, y + h, x, y, r); | |||
ctx.arcTo(x, y, x + w, y, r); | |||
ctx.closePath(); | |||
if (fill) ctx.fill(); | |||
if (stroke) ctx.stroke(); | |||
} | |||
const nodeGroup = new THREE.Group(); | |||
scene.add(nodeGroup); | |||
const positions = []; | |||
const radius = 95; | |||
// Distribute words across a sphere | |||
for (let i = 0; i < words.length; i++) { | |||
const phi = Math.acos(-1 + (2 * i + 1) / words.length); | |||
const theta = Math.sqrt(words.length * Math.PI) * phi; | |||
const x = radius * Math.cos(theta) * Math.sin(phi); | |||
const y = radius * Math.sin(theta) * Math.sin(phi); | |||
const z = radius * Math.cos(phi); | |||
positions.push(new THREE.Vector3(x, y, z)); | |||
const sprite = makeTextSprite(words[i]); | |||
sprite.position.set(x, y, z); | |||
sprite.userData.word = words[i]; | |||
nodeGroup.add(sprite); | |||
} | |||
// Connect every word to every other word | |||
const lineMaterial = new THREE.LineBasicMaterial({ | |||
color: 0x777777, | |||
transparent: true, | |||
opacity: 0.45 | |||
}); | |||
for (let i = 0; i < positions.length; i++) { | |||
for (let j = i + 1; j < positions.length; j++) { | |||
const geometry = new THREE.BufferGeometry().setFromPoints([ | |||
positions[i], | |||
positions[j] | |||
]); | |||
const line = new THREE.Line(geometry, lineMaterial); | |||
scene.add(line); | |||
} | |||
} | |||
// Small center marker | |||
const centerGeom = new THREE.SphereGeometry(3, 16, 16); | |||
const centerMat = new THREE.MeshBasicMaterial({ color: 0x222222 }); | |||
const centerDot = new THREE.Mesh(centerGeom, centerMat); | |||
scene.add(centerDot); | |||
const raycaster = new THREE.Raycaster(); | |||
const pointer = new THREE.Vector2(); | |||
const info = document.createElement('div'); | |||
info.style.position = 'absolute'; | |||
info.style.left = '12px'; | |||
info.style.top = '12px'; | |||
info.style.padding = '8px 10px'; | |||
info.style.background = 'rgba(255,255,255,0.9)'; | |||
info.style.border = '1px solid #ccc'; | |||
info.style.fontFamily = 'Arial, sans-serif'; | |||
info.style.fontSize = '14px'; | |||
info.style.color = '#111'; | |||
info.style.zIndex = '10'; | |||
info.innerHTML = 'Drag to rotate. Scroll to zoom. Click a word.'; | |||
container.appendChild(info); | |||
function onPointerMove(event) { | |||
const rect = renderer.domElement.getBoundingClientRect(); | |||
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; | |||
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; | |||
} | |||
function onClick() { | |||
raycaster.setFromCamera(pointer, camera); | |||
const intersects = raycaster.intersectObjects(nodeGroup.children, true); | |||
if (intersects.length > 0) { | |||
const obj = intersects[0].object; | |||
if (obj.userData && obj.userData.word) { | |||
info.innerHTML = 'Selected: <b>' + obj.userData.word + '</b>'; | |||
} | |||
} | |||
} | |||
renderer.domElement.addEventListener('mousemove', onPointerMove); | |||
renderer.domElement.addEventListener('click', onClick); | |||
function resize() { | |||
const w = container.clientWidth; | |||
const h = container.clientHeight; | |||
camera.aspect = w / h; | |||
camera.updateProjectionMatrix(); | |||
renderer.setSize(w, h); | |||
} | |||
window.addEventListener('resize', resize); | |||
function animate() { | |||
requestAnimationFrame(animate); | |||
controls.update(); | |||
renderer.render(scene, camera); | |||
} | |||
animate(); | |||
})(); | |||
</script> | |||
</html> | |||
Latest revision as of 15:35, 4 April 2026