|
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 = [ |
|
|
|
[255, 0, 0], |
|
[255, 85, 0], |
|
[255, 170, 0], |
|
[255, 255, 0], |
|
[170, 255, 0], |
|
[85, 255, 0], |
|
[0, 255, 0], |
|
[0, 255, 85], |
|
[0, 255, 170], |
|
[0, 255, 255], |
|
[0, 170, 255], |
|
[0, 85, 255], |
|
[0, 0, 255], |
|
[85, 0, 255], |
|
[170, 0, 255], |
|
[255, 0, 255], |
|
[255, 0, 170], |
|
[255, 0, 85] |
|
]; |
|
|
|
const limb_pairs = [ |
|
[1, 2], |
|
[1, 5], |
|
[2, 3], |
|
[3, 4], |
|
[5, 6], |
|
[6, 7], |
|
[1, 8], |
|
[8, 9], |
|
[9, 10], |
|
[1, 11], |
|
[11, 12], |
|
[12, 13], |
|
[1, 0], |
|
[0, 14], |
|
[14, 16], |
|
[0, 15], |
|
[15, 17], |
|
]; |
|
|
|
const standard_pose = [ |
|
|
|
[0.500, 0.820, 0.000], |
|
[0.500, 0.750, 0.000], |
|
[0.416, 0.750, 0.000], |
|
[0.305, 0.750, 0.000], |
|
[0.188, 0.750, 0.000], |
|
[0.584, 0.750, 0.000], |
|
[0.695, 0.750, 0.000], |
|
[0.812, 0.750, 0.000], |
|
[0.447, 0.511, 0.000], |
|
[0.453, 0.295, 0.000], |
|
[0.445, 0.109, 0.000], |
|
[0.553, 0.511, 0.000], |
|
[0.547, 0.295, 0.000], |
|
[0.555, 0.109, 0.000], |
|
[0.480, 0.848, 0.000], |
|
[0.520, 0.848, 0.000], |
|
[0.450, 0.834, 0.000], |
|
[0.550, 0.834, 0.000] |
|
] |
|
|
|
for (let xyz of standard_pose) { |
|
xyz[0] = xyz[0] - 0.5; |
|
xyz[1] = xyz[1] - 0.5; |
|
|
|
} |
|
|
|
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; |
|
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()); |
|
|
|
function set_bg(image_path, dont_dispose) { |
|
const old_tex = scene.background; |
|
if (image_path === null) { |
|
scene.background = default_bg(); |
|
if (old_tex && old_tex.dispose && !dont_dispose) old_tex.dispose(); |
|
return; |
|
} |
|
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 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(); |
|
|
|
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, |
|
}; |
|
|
|
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); |
|
|
|
|
|
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'; |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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', () => { |
|
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 <= 1) { |
|
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); |
|
|
|
|
|
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]; |
|
|
|
|
|
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); |
|
|
|
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); |
|
|
|
const onAnimateEndOneshot = []; |
|
|
|
|
|
let elliptic_limbs = ui.elliptic_limbs ? !!ui.elliptic_limbs.checked : true; |
|
|
|
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.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) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
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; |
|
|
|
let last_tick = globalThis.performance.now(); |
|
const animate = () => { |
|
const t0 = globalThis.performance.now(); |
|
|
|
|
|
|
|
|
|
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; |
|
last_tick = t0; |
|
|
|
for (let [name, body] of bodies) { |
|
const { joints, limbs, group } = body; |
|
|
|
|
|
for (let joint of joints) { |
|
joint.scale.setScalar(1 / camera.zoom); |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
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; |
|
|
|
|
|
set_pose_dict(camera, data.camera); |
|
camera.zoom = data.camera.zoom; |
|
camera.updateProjectionMatrix(); |
|
|
|
|
|
|
|
|
|
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 }; |
|
|