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 }; | |