Spaces:
Runtime error
Runtime error
| async function _import() { | |
| if (!globalThis.posex || !globalThis.posex.import) { | |
| const THREE = await import('three'); | |
| const { TrackballControls } = await import('three-trackballcontrols'); | |
| const { DragControls } = await import('three-dragcontrols'); | |
| const { MeshLine, MeshLineMaterial } = await import('three-meshline'); | |
| return { THREE, TrackballControls, DragControls, MeshLine, MeshLineMaterial }; | |
| } else { | |
| return await globalThis.posex.import(); | |
| } | |
| } | |
| const { THREE, TrackballControls, DragControls, MeshLine, MeshLineMaterial } = await _import(); | |
| const JOINT_RADIUS = 4.0; | |
| const LIMB_SIZE = 4.0; | |
| const LIMB_N = 64; | |
| const joint_names = [ | |
| 'nose', | |
| 'neck', | |
| 'right shoulder', | |
| 'right elbow', | |
| 'right wrist', | |
| 'left shoulder', | |
| 'left elbow', | |
| 'left wrist', | |
| 'right hip', | |
| 'right knee', | |
| 'right ancle', | |
| 'left hip', | |
| 'left knee', | |
| 'left ancle', | |
| 'right eye', | |
| 'left eye', | |
| 'right ear', | |
| 'left ear' | |
| ]; | |
| const joint_colors = [ | |
| // r g b | |
| [255, 0, 0], // 0: nose | |
| [255, 85, 0], // 1: neck | |
| [255, 170, 0], // 2: right shoulder | |
| [255, 255, 0], // 3: right elbow | |
| [170, 255, 0], // 4: right wrist | |
| [85, 255, 0], // 5: left shoulder | |
| [0, 255, 0], // 6: left elbow | |
| [0, 255, 85], // 7: left wrist | |
| [0, 255, 170], // 8: right hip | |
| [0, 255, 255], // 9: right knee | |
| [0, 170, 255], // 10: right ancle | |
| [0, 85, 255], // 11: left hip | |
| [0, 0, 255], // 12: left knee | |
| [85, 0, 255], // 13: left ancle | |
| [170, 0, 255], // 14: right eye | |
| [255, 0, 255], // 15: left eye | |
| [255, 0, 170], // 16: right ear | |
| [255, 0, 85] // 17: left ear | |
| ]; | |
| const limb_pairs = [ | |
| [1, 2], // 0: right shoulder | |
| [1, 5], // 1: left shoulder | |
| [2, 3], // 2: right upper arm | |
| [3, 4], // 3: right forearm | |
| [5, 6], // 4: left upper arm | |
| [6, 7], // 5: left forearm | |
| [1, 8], // 6: right hip | |
| [8, 9], // 7: right upper leg | |
| [9, 10], // 8: right lower leg | |
| [1, 11], // 9: left hip | |
| [11, 12], // 10: left upper leg | |
| [12, 13], // 11: left lower leg | |
| [1, 0], // 12: neck | |
| [0, 14], // 13: right eye | |
| [14, 16], // 14: right ear | |
| [0, 15], // 15: left eye | |
| [15, 17], // 16: left ear | |
| ]; | |
| const standard_pose = [ | |
| // x y z ∈ [0,1] | |
| [0.500, 0.820, 0.000], // 0: nose | |
| [0.500, 0.750, 0.000], // 1: neck | |
| [0.416, 0.750, 0.000], // 2: right shoulder | |
| [0.305, 0.750, 0.000], // 3: right elbow | |
| [0.188, 0.750, 0.000], // 4: right wrist | |
| [0.584, 0.750, 0.000], // 5: left shoulder | |
| [0.695, 0.750, 0.000], // 6: left elbow | |
| [0.812, 0.750, 0.000], // 7: left wrist | |
| [0.447, 0.511, 0.000], // 8: right hip | |
| [0.453, 0.295, 0.000], // 9: right knee | |
| [0.445, 0.109, 0.000], // 10: right ancle | |
| [0.553, 0.511, 0.000], // 11: left hip | |
| [0.547, 0.295, 0.000], // 12: left knee | |
| [0.555, 0.109, 0.000], // 13: left ancle | |
| [0.480, 0.848, 0.000], // 14: right eye | |
| [0.520, 0.848, 0.000], // 15: left eye | |
| [0.450, 0.834, 0.000], // 16: right ear | |
| [0.550, 0.834, 0.000] // 17: left ear | |
| ] | |
| for (let xyz of standard_pose) { | |
| xyz[0] = xyz[0] - 0.5; // [0,1] -> [-0.5,0.5] | |
| xyz[1] = xyz[1] - 0.5; // [0,1] -> [-0.5,0.5] | |
| //xyz[2] = xyz[2] * 2 - 1.0; | |
| } | |
| function create_body(unit, x0, y0, z0) { | |
| const joints = []; | |
| const limbs = []; | |
| for (let i = 0; i < standard_pose.length; ++i) { | |
| const [x, y, z] = standard_pose[i]; | |
| const [r, g, b] = joint_colors[i]; | |
| const color = (r << 16) | (g << 8) | (b << 0); | |
| const geom = new THREE.SphereGeometry(JOINT_RADIUS, 32, 32); | |
| const mat = new THREE.MeshBasicMaterial({ color: color }); | |
| const joint = new THREE.Mesh(geom, mat); | |
| joint.name = joint_names[i]; | |
| joint.position.x = x * unit + x0; | |
| joint.position.y = y * unit + y0; | |
| joint.position.z = z + z0; | |
| joint.dirty = true; // update limbs in next frame | |
| joints.push(joint); | |
| } | |
| for (let i = 0; i < limb_pairs.length; ++i) { | |
| const [r, g, b] = joint_colors[i]; | |
| const color = (r << 16) | (g << 8) | (b << 0); | |
| const line = new MeshLine(); | |
| const mat = new MeshLineMaterial({ color: color, opacity: 0.6, transparent: true }); | |
| limbs.push(new THREE.Mesh(line, mat)); | |
| } | |
| return [joints, limbs]; | |
| } | |
| function init_3d(ui) { | |
| const | |
| container = ui.container, | |
| canvas = ui.canvas, | |
| notation = ui.notation, | |
| indicator1 = ui.indicator1, | |
| indicator2 = ui.indicator2, | |
| width = () => canvas.width, | |
| height = () => canvas.height, | |
| unit = () => Math.min(width(), height()), | |
| unit_max = () => Math.max(width(), height()); | |
| canvas.addEventListener('contextmenu', e => { | |
| e.preventDefault(); | |
| }, false); | |
| const scene = new THREE.Scene(); | |
| const default_bg = () => new THREE.Color(0x000000); | |
| scene.background = default_bg(); | |
| const camera = new THREE.OrthographicCamera(width() / -2, width() / 2, height() / 2, height() / -2, 1, width() * 4); | |
| camera.fixed_roll = ui.fixed_roll ? !!ui.fixed_roll.checked : false; | |
| camera.position.z = unit_max() * 2; | |
| const renderer = new THREE.WebGLRenderer({ | |
| canvas: canvas, | |
| antialias: true, | |
| alpha: true, | |
| preserveDrawingBuffer: true, | |
| }); | |
| renderer.setSize(width(), height()); | |
| let bg_backup = new Map(); | |
| function set_bg(image_path, dont_dispose) { | |
| const old_tex = scene.background; | |
| if (dont_dispose === false) { | |
| bg_backup.clear(); | |
| } | |
| if (image_path === null) { | |
| scene.background = default_bg(); | |
| if (old_tex && old_tex.dispose && !dont_dispose) old_tex.dispose(); | |
| return; | |
| } | |
| if (!(image_path.isTexture || image_path.isColor)) { | |
| bg_backup.set('image64', image_path); | |
| } | |
| const tex = (image_path.isTexture || image_path.isColor) ? image_path : new THREE.TextureLoader().load(image_path); | |
| scene.background = tex; | |
| if (old_tex && old_tex.dispose && !dont_dispose) old_tex.dispose(); | |
| } | |
| const images = new Map(); | |
| const object_to_img = new Map(); | |
| function set_img(name, image_path , img_width, img_length) { | |
| remove_img(name); | |
| let geometry = new THREE.PlaneGeometry(img_width, img_length); | |
| let plan_texture = new THREE.TextureLoader().load(image_path) | |
| let material = new THREE.MeshBasicMaterial({//贴图通过材质添加给几何体 | |
| map: plan_texture,//给纹理属性map赋值 | |
| side: THREE.DoubleSide,//两面可见 | |
| }); | |
| let rect = new THREE.Mesh(geometry, material); | |
| const group_img = new THREE.Group(); | |
| const dispose = () => { | |
| scene.remove(group_img); | |
| }; | |
| const image = { | |
| name, | |
| group_img, | |
| img_width, | |
| img_length, | |
| rect, | |
| dispose, | |
| }; | |
| group_img.add(rect); | |
| object_to_img.set(group_img, image); | |
| images.set(name, image); | |
| scene.add(group_img); | |
| return image; | |
| } | |
| let image_num = 0; | |
| if (ui.img) { | |
| ui.img.addEventListener('change', e => { | |
| const files = ui.img.files; | |
| if (files.length != 0) { | |
| const file = files[0]; | |
| const r = new FileReader(); | |
| r.onload = () => set_img(`img_${image_num}`,r.result,30,30); | |
| r.readAsDataURL(file); | |
| } | |
| ui.img.value = ''; | |
| }, false); | |
| } | |
| if (ui.reset_img) | |
| ui.reset_img.addEventListener('click', () => { | |
| if (images.size <= 0) { | |
| ui.notify('No image is not allowed.', 'error'); | |
| return; | |
| } | |
| remove_img("img_0"); | |
| }, false); | |
| const remove_img = name => { | |
| if (!images.get(name)) return; | |
| images.get(name).dispose(); | |
| images.delete(name); | |
| }; | |
| const bodies = new Map(); | |
| let selected_body = null; | |
| let touched_body = null; | |
| const touchable_objects = []; | |
| const touchable_bodies = []; | |
| const object_to_body = new Map(); | |
| function remove(mesh) { | |
| if (mesh instanceof Array) { | |
| for (let m of mesh) remove(m); | |
| } else { | |
| mesh.material.dispose(); | |
| mesh.geometry.dispose(); | |
| object_to_body.delete(mesh); | |
| } | |
| }; | |
| const add_body = (name, x0, y0, z0) => { | |
| remove_body(name); | |
| const [joints, limbs] = create_body(unit(), x0, y0, z0); | |
| const group = new THREE.Group(); // for DragControls | |
| const dispose = () => { | |
| for (let joint of joints) { | |
| array_remove(touchable_objects, joint); | |
| remove(joint); | |
| } | |
| for (let limb of limbs) { | |
| remove(limb); | |
| scene.remove(limb); | |
| } | |
| array_remove(touchable_bodies, group); | |
| scene.remove(group); | |
| }; | |
| const reset = (dx, dy, dz) => { | |
| if (dx === undefined) dx = x0; | |
| if (dy === undefined) dy = y0; | |
| if (dz === undefined) dz = z0; | |
| for (let i = 0; i < standard_pose.length; ++i) { | |
| const [x, y, z] = standard_pose[i]; | |
| joints[i].position.set(x * unit() + dx, y * unit() + dy, z + dz); | |
| joints[i].dirty = true; | |
| } | |
| group.position.set(0, 0, 0); | |
| body.dirty = true; | |
| }; | |
| const body = { | |
| name, | |
| group, | |
| joints, | |
| limbs, | |
| x0, y0, z0, | |
| dispose, | |
| reset, | |
| dirty: true, // update limbs in next frame | |
| }; | |
| for (let joint of joints) { | |
| touchable_objects.push(joint); | |
| object_to_body.set(joint, body); | |
| group.add(joint); | |
| } | |
| for (let limb of limbs) { | |
| scene.add(limb); | |
| object_to_body.set(limb, body); | |
| } | |
| object_to_body.set(group, body); | |
| bodies.set(name, body); | |
| scene.add(group); | |
| touchable_bodies.push(group); | |
| return body; | |
| }; | |
| const remove_body = name => { | |
| if (!bodies.get(name)) return; | |
| bodies.get(name).dispose(); | |
| bodies.delete(name); | |
| }; | |
| const get_body_rect = body => { | |
| const v = new THREE.Vector3(); | |
| let xmin = Infinity, xmax = -Infinity, ymin = Infinity, ymax = -Infinity; | |
| for (let joint of body.joints) { | |
| const wpos = joint.getWorldPosition(v); | |
| const spos = wpos.project(camera); | |
| if (spos.x < xmin) xmin = spos.x; | |
| if (xmax < spos.x) xmax = spos.x; | |
| if (spos.y < ymin) ymin = spos.y; | |
| if (ymax < spos.y) ymax = spos.y; | |
| } | |
| return [xmin, ymin, xmax, ymax]; | |
| }; | |
| // const default_body = add_body('defualt', 0, 0, 0); | |
| const controls = new TrackballControls(camera, renderer.domElement); | |
| const dragger_joint = new DragControls(touchable_objects, camera, renderer.domElement); | |
| const dragger_body = new DragControls([], camera, renderer.domElement); | |
| dragger_body.transformGroup = true; | |
| dragger_joint.addEventListener('dragstart', () => { controls.enabled = false; }); | |
| dragger_joint.addEventListener('dragend', () => { controls.enabled = true; }); | |
| dragger_joint.addEventListener('drag', e => { | |
| e.object.dirty = true; | |
| object_to_body.get(e.object).dirty = true; | |
| }); | |
| dragger_body.addEventListener('dragstart', () => { controls.enabled = false; }); | |
| dragger_body.addEventListener('dragend', () => { controls.enabled = true; }); | |
| dragger_body.addEventListener('drag', e => { | |
| const body = object_to_body.get(e.object); | |
| body.dirty = true; | |
| for (let i = 0; i < body.joints.length; ++i) { | |
| body.joints[i].dirty = true; | |
| } | |
| }); | |
| renderer.domElement.addEventListener('pointerdown', e => { | |
| dragger_joint.enabled = e.button === 0; | |
| dragger_body.enabled = e.button === 2; | |
| }, true); | |
| const rc = new THREE.Raycaster(); | |
| const m = new THREE.Vector2(); | |
| renderer.domElement.addEventListener('pointermove', e => { | |
| e.preventDefault(); | |
| m.x = (e.offsetX / width()) * 2 - 1; | |
| m.y = (1 - e.offsetY / height()) * 2 - 1; | |
| rc.setFromCamera(m, camera); | |
| const touched = rc.intersectObjects(touchable_objects); | |
| // show label | |
| if (touched.length != 0) { | |
| const [dx, dy] = get_relative_offset(renderer.domElement, container); | |
| notation.textContent = touched[0].object.name; | |
| notation.style.left = `${e.offsetX + dx}px`; | |
| notation.style.top = `${e.offsetY + dy - 32}px`; | |
| notation.style.display = 'block'; | |
| } else { | |
| notation.textContent = ''; | |
| notation.style.display = 'none'; | |
| } | |
| // show temporary selection | |
| if (touched.length != 0) { | |
| touched_body = object_to_body.get(touched[0].object); | |
| } else { | |
| touched_body = null; | |
| } | |
| }, false); | |
| renderer.domElement.addEventListener('pointerdown', e => { | |
| e.preventDefault(); | |
| m.x = (e.offsetX / width()) * 2 - 1; | |
| m.y = (1 - e.offsetY / height()) * 2 - 1; | |
| rc.setFromCamera(m, camera); | |
| const touched = rc.intersectObjects(touchable_objects); | |
| // show selection | |
| if (touched.length != 0) { | |
| selected_body = object_to_body.get(touched[0].object); | |
| const objs = dragger_body.getObjects(); | |
| objs.length = 0; | |
| objs.push(selected_body.group); | |
| dragger_body.onPointerDown(e); | |
| } else { | |
| selected_body = null; | |
| dragger_body.getObjects().length = 0; | |
| } | |
| }, false); | |
| if (ui.all_reset) | |
| ui.all_reset.addEventListener('click', () => { | |
| touched_body = null; | |
| selected_body = null; | |
| camera.position.set(0, 0, unit_max() * 2); | |
| camera.rotation.set(0, 0, 0); | |
| controls.reset(); | |
| for (let name of Array.from(bodies.keys()).slice(1)) { | |
| remove_body(name); | |
| } | |
| for (let body of bodies.values()) { | |
| body.reset(0, 0, 0); | |
| } | |
| }, false); | |
| if (ui.reset_camera) | |
| ui.reset_camera.addEventListener('click', () => { | |
| camera.position.set(0, 0, unit_max() * 2); | |
| camera.rotation.set(0, 0, 0); | |
| controls.reset(); | |
| }, false); | |
| if (ui.reset_pose) | |
| ui.reset_pose.addEventListener('click', () => { | |
| if (selected_body) { | |
| selected_body.reset(); | |
| } else { | |
| for (let [name, body] of bodies) { | |
| body.reset(); | |
| } | |
| } | |
| }, false); | |
| if (ui.fixed_roll) | |
| ui.fixed_roll.addEventListener('change', () => { | |
| camera.fixed_roll = !!ui.fixed_roll.checked; | |
| }, false); | |
| let body_num = 1; | |
| if (ui.add_body) | |
| ui.add_body.addEventListener('click', () => { | |
| if (bodies.size < 1) { | |
| add_body('defualt', 0, 0, 0); | |
| } else { | |
| const last_body = selected_body ?? Array.from(bodies.values()).at(-1); | |
| const base = last_body.joints[0].getWorldPosition(new THREE.Vector3()); | |
| const | |
| dx = base.x - standard_pose[0][0] * unit(), | |
| dy = base.y - standard_pose[0][1] * unit(), | |
| dz = base.z - standard_pose[0][2]; | |
| add_body(`body_${body_num++}`, dx + 32, dy, dz); | |
| } | |
| }, false); | |
| if (ui.remove_body) | |
| ui.remove_body.addEventListener('click', () => { | |
| if (!selected_body) { | |
| ui.notify('No body is selected.', 'error'); | |
| return; | |
| } | |
| if (bodies.size <= 0) { | |
| ui.notify('No body is not allowed.', 'error'); | |
| return; | |
| } | |
| remove_body(selected_body.name); | |
| touched_body = null; | |
| selected_body = null; | |
| }, false); | |
| const get_client_boundary = body => { | |
| let [xmin, ymin, xmax, ymax] = get_body_rect(body); | |
| // [-1,1] -> [0,width] | |
| xmin = (xmin + 1) * width() / 2; | |
| xmax = (xmax + 1) * width() / 2; | |
| ymin = height() - (ymin + 1) * height() / 2; | |
| ymax = height() - (ymax + 1) * height() / 2; | |
| [ymin, ymax] = [ymax, ymin]; | |
| // add margin | |
| xmin = xmin - 5 + renderer.domElement.offsetLeft; | |
| xmax = xmax + 5 + renderer.domElement.offsetLeft; | |
| ymin = ymin - 5 + renderer.domElement.offsetTop; | |
| ymax = ymax + 5 + renderer.domElement.offsetTop; | |
| return [xmin, ymin, xmax, ymax]; | |
| } | |
| const size_change = (w, h) => { | |
| if (w < 64 || h < 64) return; | |
| canvas.width = w; | |
| canvas.height = h; | |
| renderer.setSize(w, h); | |
| // update camera | |
| camera.left = w / -2; | |
| camera.right = w / 2; | |
| camera.top = h / 2; | |
| camera.bottom = h / -2; | |
| camera.near = 1; | |
| camera.far = w * 4; | |
| camera.position.z = unit_max() * 2; | |
| camera.updateProjectionMatrix(); | |
| controls.handleResize(); | |
| }; | |
| const width_input = ui.canvas_width, height_input = ui.canvas_height; | |
| if (width_input && height_input) { | |
| width_input.addEventListener('change', () => { | |
| const w = +width_input.value; | |
| const h = +height_input.value; | |
| size_change(w, h); | |
| }, false); | |
| height_input.addEventListener('change', () => { | |
| const w = +width_input.value; | |
| const h = +height_input.value; | |
| size_change(w, h); | |
| }, false); | |
| } | |
| if (ui.bg) | |
| ui.bg.addEventListener('change', e => { | |
| const files = ui.bg.files; | |
| if (files.length != 0) { | |
| const file = files[0]; | |
| const r = new FileReader(); | |
| r.onload = () => set_bg(r.result); | |
| r.readAsDataURL(file); | |
| } | |
| ui.bg.value = ''; | |
| }, false); | |
| if (ui.reset_bg) | |
| ui.reset_bg.addEventListener('click', () => set_bg(null), false); | |
| function get_pose_dict(obj3d) { | |
| return { | |
| position: obj3d.position.toArray(), | |
| rotation: obj3d.rotation.toArray(), | |
| scale: obj3d.scale.toArray(), | |
| up: obj3d.up.toArray(), | |
| }; | |
| } | |
| function set_pose_dict(obj3d, dict) { | |
| obj3d.position.set(...dict.position); | |
| obj3d.rotation.set(...dict.rotation); | |
| obj3d.scale.set(...dict.scale); | |
| obj3d.up.set(...dict.up); | |
| } | |
| if (ui.save_pose && ui.save_pose_callback) | |
| ui.save_pose.addEventListener('click', async () => { | |
| const name = prompt('Input pose name.'); | |
| if (name === undefined || name === null || name === '') return; | |
| const screen = { | |
| width: width(), | |
| height: height(), | |
| } | |
| const camera_ = get_pose_dict(camera); | |
| camera_.zoom = camera.zoom; | |
| const joints = []; | |
| for (let [name, body] of bodies) { | |
| joints.push({ | |
| name, | |
| joints: body.joints.map(j => get_pose_dict(j)), | |
| group: get_pose_dict(body.group), | |
| x0: body.x0, | |
| y0: body.y0, | |
| z0: body.z0, | |
| }); | |
| } | |
| const image = await ui.getDataURL(); | |
| const data = { name, image, screen, camera: camera_, joints }; | |
| const result = await ui.save_pose_callback(data); | |
| ui.notify(result.result, result.ok ? 'success' : 'error'); | |
| }, false); | |
| if (ui.get_imgs_callback()) { | |
| gradioApp().querySelector(`#posex-t2i-generate`).addEventListener('click', async () => { | |
| //搜集参数 | |
| const bgImg = bg_backup.get("image64") === "" ? "" : bg_backup.get("image64"); | |
| console.log(`bgImgsize:${bgImg.length}`); | |
| const maskImg = await ui.getDataURL(); | |
| console.log(`maskImg:${maskImg.length}`); | |
| const data = { bgImg, maskImg }; | |
| const result = await ui.get_imgs_callback(data); | |
| ui.notify(result.result, result.ok ? 'success' : 'error'); | |
| }, false) | |
| // ui.get_imgs.addEventListener('click', async () => { | |
| // //搜集参数 | |
| // const bgImg = bg_backup.get("image64") === "" ? "" : bg_backup.get("image64"); | |
| // console.log(`bgImgsize:${bgImg.length}`); | |
| // const maskImg = await ui.getDataURL(); | |
| // console.log(`maskImg:${maskImg.length}`); | |
| // const data = { bgImg, maskImg }; | |
| // const result = await ui.get_imgs_callback(data); | |
| // ui.notify(result.result, result.ok ? 'success' : 'error'); | |
| // }, false) | |
| } | |
| const onAnimateEndOneshot = []; | |
| // joint and limb update | |
| let elliptic_limbs = ui.elliptic_limbs ? !!ui.elliptic_limbs.checked : true; | |
| //let joint_size_m = ui.joint_radius ? +ui.joint_radius.value / JOINT_RADIUS : 1.0; | |
| let limb_size_m = ui.limb_width ? +ui.limb_width.value / LIMB_SIZE : 1.0; | |
| if (ui.elliptic_limbs) | |
| ui.elliptic_limbs.addEventListener('change', () => { | |
| const b = !!ui.elliptic_limbs.checked; | |
| if (elliptic_limbs !== b) { | |
| elliptic_limbs = b; | |
| for (let body of bodies.values()) { | |
| body.dirty = true; | |
| for (let i = 0; i < body.joints.length; ++i) { | |
| body.joints[i].dirty = true; | |
| } | |
| } | |
| } | |
| }, false); | |
| //if (ui.joint_radius) | |
| // ui.joint_radius.addEventListener('input', () => { | |
| // const new_val = +ui.joint_radius.value / JOINT_RADIUS; | |
| // if (joint_size_m !== new_val) { | |
| // joint_size_m = new_val; | |
| // for (let body of bodies.values()) { | |
| // body.dirty = true; | |
| // for (let i = 0; i < body.joints.length; ++i) { | |
| // body.joints[i].dirty = true; | |
| // } | |
| // } | |
| // } | |
| // }, false); | |
| if (ui.limb_width) | |
| ui.limb_width.addEventListener('input', () => { | |
| const new_val = +ui.limb_width.value / LIMB_SIZE; | |
| if (limb_size_m !== new_val) { | |
| limb_size_m = new_val; | |
| for (let body of bodies.values()) { | |
| body.dirty = true; | |
| for (let i = 0; i < body.joints.length; ++i) { | |
| body.joints[i].dirty = true; | |
| } | |
| } | |
| } | |
| }, false); | |
| const limb_vecs = Array.from(Array(LIMB_N)).map(x => new THREE.Vector3()); | |
| function elliptic_limb_width(p) { | |
| // draw limb ellipse | |
| // x^2 / a^2 + y^2 / b^2 = 1 | |
| // a := half of distance between two joints | |
| // b := 2 * LIMB_SIZE / camera.zoom | |
| // {a(2p-1)}^2 / a^2 + y^2 / b^2 = 1 | |
| // y^2 = b^2 { 1 - (2p-1)^2 } | |
| const b = 2 * LIMB_SIZE * limb_size_m / camera.zoom; | |
| const pp = 2 * p - 1; | |
| return b * Math.sqrt(1 - pp * pp); | |
| } | |
| function stick_limb_width(p) { | |
| // half width of ellipse | |
| return LIMB_SIZE * limb_size_m / camera.zoom; | |
| } | |
| function create_limb(mesh, from, to) { | |
| const s0 = limb_vecs[0]; | |
| const s1 = limb_vecs[LIMB_N - 1]; | |
| from.getWorldPosition(s0); | |
| to.getWorldPosition(s1); | |
| const N = LIMB_N - 1; | |
| for (let i = 1; i < limb_vecs.length - 1; ++i) { | |
| limb_vecs[i].lerpVectors(s0, s1, i / N); | |
| } | |
| mesh.geometry.setPoints(limb_vecs, elliptic_limbs ? elliptic_limb_width : stick_limb_width); | |
| } | |
| let low_fps = ui.low_fps ? !!ui.low_fps.checked : false; | |
| if (ui.low_fps) | |
| ui.low_fps.addEventListener('change', () => { | |
| low_fps = !!ui.low_fps.checked; | |
| }, false); | |
| let last_zoom = camera.zoom; | |
| let running = true; | |
| //const frames = [0,0,0,0,0,0,0,0,0,0], frame_index = 0; | |
| let last_tick = globalThis.performance.now(); | |
| const animate = () => { | |
| const t0 = globalThis.performance.now(); | |
| //frames[(frame_index++)%frames.length] = t0 - last_tick; | |
| //last_tick = t0; | |
| //console.log(frames.reduce((acc, cur) => acc + cur) / frames.length); | |
| requestAnimationFrame(animate); | |
| if (!running) return; | |
| if (controls.enabled) { | |
| if (controls.screen.width === 0 && controls.screen.height === 0) { | |
| controls.handleResize(); | |
| } | |
| } | |
| controls.update(); | |
| if (low_fps && t0 - last_tick < 30) return; // nearly 30fps | |
| last_tick = t0; | |
| for (let [name, body] of bodies) { | |
| const { joints, limbs, group } = body; | |
| // update joint size | |
| for (let joint of joints) { | |
| joint.scale.setScalar(1 / camera.zoom); | |
| } | |
| // show limbs | |
| const zoom_changed = last_zoom !== camera.zoom; | |
| if (body.dirty || zoom_changed) { | |
| for (let i = 0; i < limb_pairs.length; ++i) { | |
| const [from_index, to_index] = limb_pairs[i]; | |
| const [from, to] = [joints[from_index], joints[to_index]]; | |
| if (from.dirty || to.dirty || zoom_changed) { | |
| create_limb(limbs[i], from, to); | |
| } | |
| } | |
| for (let i = 0; i < joints.length; ++i) { | |
| joints[i].dirty = false; | |
| } | |
| body.dirty = false; | |
| } | |
| } | |
| last_zoom = camera.zoom; | |
| // show selection | |
| if (touched_body) { | |
| let [xmin, ymin, xmax, ymax] = get_client_boundary(touched_body); | |
| const st = indicator2.style; | |
| st.display = 'block'; | |
| st.left = `${xmin}px`; | |
| st.top = `${ymin}px`; | |
| st.width = `${xmax - xmin}px`; | |
| st.height = `${ymax - ymin}px`; | |
| } else { | |
| indicator2.style.display = 'none'; | |
| } | |
| if (selected_body) { | |
| let [xmin, ymin, xmax, ymax] = get_client_boundary(selected_body); | |
| const st = indicator1.style; | |
| st.display = 'block'; | |
| st.left = `${xmin}px`; | |
| st.top = `${ymin}px`; | |
| st.width = `${xmax - xmin}px`; | |
| st.height = `${ymax - ymin}px`; | |
| } else { | |
| indicator1.style.display = 'none'; | |
| } | |
| if (camera.fixed_roll) camera.up.set(0, 1, 0); | |
| renderer.render(scene, camera); | |
| for (let fn of onAnimateEndOneshot) { | |
| fn(); | |
| } | |
| onAnimateEndOneshot.length = 0; | |
| }; | |
| ui.loadPose = function (data) { | |
| selected_body = null; | |
| touched_body = null; | |
| touchable_objects.length = 0; | |
| touchable_bodies.length = 0; | |
| object_to_body.clear(); | |
| for (let name of bodies.keys()) { | |
| remove_body(name); | |
| } | |
| // screen | |
| size_change(data.screen.width, data.screen.height); | |
| if (width_input) width_input.value = data.screen.width; | |
| if (height_input) height_input.value = data.screen.height; | |
| // camera | |
| set_pose_dict(camera, data.camera); | |
| camera.zoom = data.camera.zoom; | |
| camera.updateProjectionMatrix(); | |
| // bodies | |
| // update `body_num` | |
| const body_names = data.joints.map(x => { | |
| const m = /^body_(\d+)$/.exec(x.name); | |
| return m ? +m[1] : -1; | |
| }).filter(x => 0 <= x); | |
| if (body_names.length == 0) { | |
| body_num = 0; | |
| } else { | |
| body_num = Math.max(...body_names) + 1; | |
| } | |
| for (let dict of data.joints) { | |
| const body = add_body(dict.name, dict.x0, dict.y0, dict.z0); | |
| for (let i = 0, e = Math.min(body.joints.length, dict.joints.length); i < e; ++i) { | |
| set_pose_dict(body.joints[i], dict.joints[i]); | |
| } | |
| set_pose_dict(body.group, dict.group); | |
| } | |
| }; | |
| ui.getDataURL = async function () { | |
| const pr = new Promise(resolve => { | |
| const current_bg = scene.background; | |
| set_bg(null, true); | |
| onAnimateEndOneshot.push(() => { | |
| resolve(renderer.domElement.toDataURL('image/png')); | |
| set_bg(current_bg); | |
| }); | |
| }); | |
| return await pr; | |
| }; | |
| ui.getBlob = async function () { | |
| const pr = new Promise(resolve => { | |
| const current_bg = scene.background; | |
| set_bg(null, true); | |
| onAnimateEndOneshot.push(() => { | |
| renderer.domElement.toBlob(blob => { | |
| resolve(blob); | |
| set_bg(current_bg); | |
| }); | |
| }); | |
| }); | |
| return await pr; | |
| }; | |
| ui.stop = function () { | |
| running = false; | |
| dragger_joint.deactivate(); | |
| dragger_joint.enabled = false; | |
| dragger_body.deactivate(); | |
| dragger_body.enabled = false; | |
| controls.enabled = false; | |
| }; | |
| ui.play = function () { | |
| running = true; | |
| dragger_joint.activate(); | |
| dragger_joint.enabled = true; | |
| dragger_body.activate(); | |
| dragger_body.enabled = true; | |
| controls.enabled = true; | |
| controls.handleResize(); | |
| }; | |
| return animate; | |
| } | |
| function init(ui) { | |
| if (ui.save) | |
| ui.save.addEventListener('click', async () => { | |
| const a = document.createElement('a'); | |
| if (ui.getDataURL) { | |
| a.href = await ui.getDataURL('image/png'); | |
| } else { | |
| a.href = ui.canvas.toDataURL('image/png'); | |
| } | |
| a.download = 'download.png'; | |
| a.click(); | |
| ui.notify('save success'); | |
| }, false); | |
| if (ui.copy) | |
| ui.copy.addEventListener('click', async () => { | |
| if (globalThis.ClipboardItem === undefined) { | |
| alert('`ClipboardItem` is not defined. If you are in Firefox, change about:config -> dom.events.asyncClipboard.clipboardItem to `true`.') | |
| return; | |
| } | |
| async function get_blob() { | |
| if (ui.getBlob) { | |
| return await ui.getBlob(); | |
| } else { | |
| return await new Promise(resolve => ui.canvas.toBlob(blob => resolve(blob))); | |
| } | |
| } | |
| try { | |
| const blob = await get_blob(); | |
| const data = new ClipboardItem({ [blob.type]: blob }); | |
| navigator.clipboard.write([data]); | |
| ui.notify('copy success'); | |
| } catch (e) { | |
| ui.notify(`failed to copy data: ${e.message}`, 'error'); | |
| } | |
| }, false); | |
| } | |
| function array_remove(array, item) { | |
| let index = array.indexOf(item); | |
| while (0 <= index) { | |
| array.splice(index, 1); | |
| index = array.indexOf(item); | |
| } | |
| } | |
| function get_relative_offset(target, origin) { | |
| const r0 = origin.getBoundingClientRect(); | |
| const r1 = target.getBoundingClientRect(); | |
| return [r1.left - r0.left, r1.top - r0.top]; | |
| } | |
| export { init, init_3d }; | |