park_kit/index.html
type-two 153e26c329 park_kit v0.1 — 27 textured park props + 18 MODELBEAST surfaces + manifest
propkit.py: textured-GLB writer (TEXCOORD_0, shared external textures,
MIRRORED_REPEAT). prop_author.py: parametric generators — ramps, rails,
furniture, trees, dressing — each with local-space grind metadata and
skatemakerpro element/collider hints.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 19:47:34 +10:00

141 lines
5.6 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>park_kit — catalog</title>
<style>
html,body{margin:0;height:100%;overflow:hidden;background:#0d0f0e;font-family:ui-monospace,Menlo,monospace}
#hud{position:fixed;top:10px;left:10px;color:#cfe8c9;z-index:5;font-size:13px}
#hud select{background:#1b241c;color:#cfe8c9;border:1px solid #3c5c40;font:inherit;padding:3px 6px}
#hud label{margin-left:10px;user-select:none}
#info{position:fixed;bottom:10px;left:10px;color:#8fae89;z-index:5;font-size:12px;white-space:pre}
</style>
</head>
<body>
<div id="hud">
<b>park_kit</b>
<select id="pick"><option value="__all__">— all props —</option></select>
<label><input type="checkbox" id="grinds" checked> grind edges</label>
</div>
<div id="info"></div>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
const tl = new THREE.TextureLoader();
const sky = tl.load('textures/sky_day.jpg');
sky.colorSpace = THREE.SRGBColorSpace;
scene.background = sky;
const cam = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.05, 300);
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
const ctl = new OrbitControls(cam, renderer.domElement);
scene.add(new THREE.HemisphereLight(0xdfeeff, 0x3a4a35, 1.05));
const sun = new THREE.DirectionalLight(0xfff2dd, 2.2);
sun.position.set(18, 30, 12); sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
Object.assign(sun.shadow.camera, { left: -30, right: 30, top: 30, bottom: -30 });
scene.add(sun);
// ground: green paddo concrete
const gtex = tl.load('textures/concrete_green.jpg');
gtex.colorSpace = THREE.SRGBColorSpace;
gtex.wrapS = gtex.wrapT = THREE.MirroredRepeatWrapping;
gtex.repeat.set(30, 30);
const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100),
new THREE.MeshStandardMaterial({ map: gtex, roughness: 0.92 }));
ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true;
scene.add(ground);
const manifest = await (await fetch('manifest.json')).json();
const pick = document.getElementById('pick');
for (const p of manifest.props) {
const o = document.createElement('option');
o.value = p.id; o.textContent = `${p.id} (${p.category})`;
pick.appendChild(o);
}
const loader = new GLTFLoader();
const grp = new THREE.Group(); scene.add(grp);
const grindGrp = new THREE.Group(); scene.add(grindGrp);
const grindMat = new THREE.LineBasicMaterial({ color: 0xff2d55 });
function label(text, x, z) {
const c = document.createElement('canvas'); c.width = 512; c.height = 96;
const g = c.getContext('2d');
g.fillStyle = '#0d0f0e'; g.globalAlpha = 0.55; g.fillRect(0, 0, 512, 96);
g.globalAlpha = 1; g.fillStyle = '#d8f2cf';
g.font = 'bold 44px Menlo,monospace'; g.textAlign = 'center';
g.fillText(text, 256, 62);
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t }));
s.scale.set(2.6, 0.5, 1); s.position.set(x, 0.28, z + 1.9);
return s;
}
function addGrinds(p, x, z, ry) {
for (const gr of p.grinds || []) {
const rot = ([px, pz]) => [px * Math.cos(ry) + pz * Math.sin(ry),
-px * Math.sin(ry) + pz * Math.cos(ry)];
const [ax, az] = rot(gr.a), [bx, bz] = rot(gr.b);
const geo = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(x + ax, gr.ya + 0.015, z + az),
new THREE.Vector3(x + bx, gr.yb + 0.015, z + bz)]);
grindGrp.add(new THREE.Line(geo, grindMat));
}
}
let placed = 0;
function show(id) {
grp.clear(); grindGrp.clear(); placed = 0;
const items = id === '__all__' ? manifest.props
: manifest.props.filter(p => p.id === id);
const cols = Math.ceil(Math.sqrt(items.length));
const pitch = 7.5;
items.forEach((p, i) => {
const x = id === '__all__' ? (i % cols - (cols - 1) / 2) * pitch : 0;
const z = id === '__all__' ? (Math.floor(i / cols) - (cols - 1) / 2) * pitch : 0;
loader.load(p.src, g => {
g.scene.traverse(o => { if (o.isMesh) { o.castShadow = o.receiveShadow = true; } });
g.scene.position.set(x, 0, z);
grp.add(g.scene);
grp.add(label(p.id, x, z));
addGrinds(p, x, z, 0);
placed++;
});
});
const dim = id === '__all__' ? cols * pitch
: Math.max(...(items[0].size || [4]), 2.5) * 1.35 + 1.5;
cam.position.set(dim * 0.55, dim * 0.45, dim * 0.85);
ctl.target.set(0, id === '__all__' ? 0 : (items[0].size?.[1] || 1.6) * 0.45, 0);
document.getElementById('info').textContent =
id === '__all__' ? `${items.length} props — ${manifest.note.split('.')[0]}`
: JSON.stringify(items[0], null, 1);
}
pick.onchange = () => show(pick.value);
document.getElementById('grinds').onchange = e =>
grindGrp.visible = e.target.checked;
show('__all__');
window.__ready = () => placed;
window.__view = id => { pick.value = id; show(id); };
addEventListener('resize', () => {
cam.aspect = innerWidth / innerHeight; cam.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
(function loop() { requestAnimationFrame(loop); ctl.update(); renderer.render(scene, cam); })();
window.__snap = () => { ctl.update(); renderer.render(scene, cam); return true; };
</script>
</body>
</html>