|
| 1 | +import {tiny} from '../tiny-graphics.js'; |
| 2 | +// Pull these names into this module's scope for convenience: |
| 3 | +const {Vector, Vector3, vec, vec3, vec4, color, Matrix, Mat4, Shape, Shader, Component} = tiny; |
| 4 | + |
| 5 | +const defs = {}; |
| 6 | + |
| 7 | +export {tiny, defs}; |
| 8 | + |
| 9 | +const Minimal_Webgl_Demo = defs.Minimal_Webgl_Demo = |
| 10 | + class Minimal_Webgl_Demo extends Component { |
| 11 | + init () { |
| 12 | + this.widget_options = {make_controls: false}; // This demo is too minimal to have controls |
| 13 | + this.shapes = {triangle: new Minimal_Shape ()}; |
| 14 | + this.shader = new Basic_Shader (); |
| 15 | + } |
| 16 | + render_animation (caller) { |
| 17 | + this.shapes.triangle.draw (caller, this.uniforms, Mat4.identity (), {shader: this.shader}); |
| 18 | + } |
| 19 | + }; |
| 20 | + |
| 21 | +const Movement_Controls = defs.Movement_Controls = |
| 22 | + class Movement_Controls extends Component { |
| 23 | + roll = 0; |
| 24 | + look_around_locked = true; |
| 25 | + thrust = vec3 (0, 0, 0); |
| 26 | + pos = vec3 (0, 0, 0); |
| 27 | + z_axis = vec3 (0, 0, 0); |
| 28 | + radians_per_frame = 1 / 200; |
| 29 | + meters_per_frame = 20; |
| 30 | + speed_multiplier = 1; |
| 31 | + mouse_enabled_canvases = new Set (); |
| 32 | + will_take_over_uniforms = true; |
| 33 | + set_recipient (matrix_closure, inverse_closure) { |
| 34 | + this.matrix = matrix_closure; |
| 35 | + this.inverse = inverse_closure; |
| 36 | + } |
| 37 | + reset () { |
| 38 | + this.set_recipient (() => this.uniforms.camera_transform, |
| 39 | + () => this.uniforms.camera_inverse); |
| 40 | + } |
| 41 | + add_mouse_controls (canvas) { |
| 42 | + if (this.mouse_enabled_canvases.has (canvas)) |
| 43 | + return; |
| 44 | + this.mouse_enabled_canvases.add (canvas); |
| 45 | + // First, measure mouse steering, for rotating the flyaround camera: |
| 46 | + this.mouse = {"from_center": vec (0, 0)}; |
| 47 | + const mouse_position = (e, rect = canvas.getBoundingClientRect ()) => |
| 48 | + vec (e.clientX - (rect.left + rect.right) / 2, e.clientY - (rect.bottom + rect.top) / 2); |
| 49 | + // Set up mouse response. The last one stops us from reacting if the mouse leaves the canvas: |
| 50 | + document.addEventListener ("mouseup", e => { this.mouse.anchor = undefined; }); |
| 51 | + canvas.addEventListener ("mousedown", e => { |
| 52 | + e.preventDefault (); |
| 53 | + this.mouse.anchor = mouse_position (e); |
| 54 | + }); |
| 55 | + canvas.addEventListener ("mousemove", e => { |
| 56 | + e.preventDefault (); |
| 57 | + this.mouse.from_center = mouse_position (e); |
| 58 | + }); |
| 59 | + canvas.addEventListener ("mouseout", e => { if ( !this.mouse.anchor) this.mouse.from_center.scale_by (0); }); |
| 60 | + } |
| 61 | + render_explanation (document_builder, document_element = document_builder.document_region) { } |
| 62 | + render_controls () { |
| 63 | + this.control_panel.innerHTML += "Click and drag the scene to <br> spin your viewpoint around it.<br>"; |
| 64 | + this.key_triggered_button ("Up", [" "], () => this.thrust[ 1 ] = -1, undefined, () => this.thrust[ 1 ] = 0); |
| 65 | + this.key_triggered_button ("Forward", ["w"], () => this.thrust[ 2 ] = 1, undefined, |
| 66 | + () => this.thrust[ 2 ] = 0); |
| 67 | + this.new_line (); |
| 68 | + this.key_triggered_button ("Left", ["a"], () => this.thrust[ 0 ] = 1, undefined, () => this.thrust[ 0 ] = 0); |
| 69 | + this.key_triggered_button ("Back", ["s"], () => this.thrust[ 2 ] = -1, undefined, () => this.thrust[ 2 ] = 0); |
| 70 | + this.key_triggered_button ("Right", ["d"], () => this.thrust[ 0 ] = -1, undefined, |
| 71 | + () => this.thrust[ 0 ] = 0); |
| 72 | + this.new_line (); |
| 73 | + this.key_triggered_button ("Down", ["z"], () => this.thrust[ 1 ] = 1, undefined, () => this.thrust[ 1 ] = 0); |
| 74 | + |
| 75 | + const speed_controls = this.control_panel.appendChild (document.createElement ("span")); |
| 76 | + speed_controls.style.margin = "30px"; |
| 77 | + this.key_triggered_button ("-", ["o"], () => |
| 78 | + this.speed_multiplier /= 1.2, "green", undefined, undefined, speed_controls); |
| 79 | + this.live_string (box => { box.textContent = "Speed: " + this.speed_multiplier.toFixed (2); }, |
| 80 | + speed_controls); |
| 81 | + this.key_triggered_button ("+", ["p"], () => |
| 82 | + this.speed_multiplier *= 1.2, "green", undefined, undefined, speed_controls); |
| 83 | + this.new_line (); |
| 84 | + this.key_triggered_button ("Roll left", [","], () => this.roll = 1, undefined, () => this.roll = 0); |
| 85 | + this.key_triggered_button ("Roll right", ["."], () => this.roll = -1, undefined, () => this.roll = 0); |
| 86 | + this.new_line (); |
| 87 | + this.key_triggered_button ("(Un)freeze mouse look around", ["f"], () => this.look_around_locked ^= 1, |
| 88 | + "green"); |
| 89 | + this.new_line (); |
| 90 | + this.live_string ( |
| 91 | + box => box.textContent = "Position: " + this.pos[ 0 ].toFixed (2) + ", " + this.pos[ 1 ].toFixed (2) |
| 92 | + + ", " + this.pos[ 2 ].toFixed (2)); |
| 93 | + this.new_line (); |
| 94 | + // The facing directions are surprisingly affected by the left hand rule: |
| 95 | + this.live_string (box => box.textContent = "Facing: " + ((this.z_axis[ 0 ] > 0 ? "West " : "East ") |
| 96 | + + (this.z_axis[ 1 ] > 0 ? "Down " : "Up ") + |
| 97 | + (this.z_axis[ 2 ] > 0 ? "North" : "South"))); |
| 98 | + this.new_line (); |
| 99 | + this.key_triggered_button ("Go to world origin", ["r"], () => { |
| 100 | + this.matrix ().set_identity (4, 4); |
| 101 | + this.inverse ().set_identity (4, 4); |
| 102 | + }, "orange"); |
| 103 | + this.new_line (); |
| 104 | + |
| 105 | + this.key_triggered_button ("Look at origin from front", ["1"], () => { |
| 106 | + this.inverse ().set (Mat4.look_at (vec3 (0, 0, 10), vec3 (0, 0, 0), vec3 (0, 1, 0))); |
| 107 | + this.matrix ().set (Mat4.inverse (this.inverse ())); |
| 108 | + }, "black"); |
| 109 | + this.new_line (); |
| 110 | + this.key_triggered_button ("from right", ["2"], () => { |
| 111 | + this.inverse ().set (Mat4.look_at (vec3 (10, 0, 0), vec3 (0, 0, 0), vec3 (0, 1, 0))); |
| 112 | + this.matrix ().set (Mat4.inverse (this.inverse ())); |
| 113 | + }, "black"); |
| 114 | + this.key_triggered_button ("from rear", ["3"], () => { |
| 115 | + this.inverse ().set (Mat4.look_at (vec3 (0, 0, -10), vec3 (0, 0, 0), vec3 (0, 1, 0))); |
| 116 | + this.matrix ().set (Mat4.inverse (this.inverse ())); |
| 117 | + }, "black"); |
| 118 | + this.key_triggered_button ("from left", ["4"], () => { |
| 119 | + this.inverse ().set (Mat4.look_at (vec3 (-10, 0, 0), vec3 (0, 0, 0), vec3 (0, 1, 0))); |
| 120 | + this.matrix ().set (Mat4.inverse (this.inverse ())); |
| 121 | + }, "black"); |
| 122 | + this.new_line (); |
| 123 | + this.key_triggered_button ("Attach to global camera", ["Shift", "R"], |
| 124 | + () => { this.will_take_over_uniforms = true; }, "blue"); |
| 125 | + this.new_line (); |
| 126 | + } |
| 127 | + first_person_flyaround (radians_per_frame, meters_per_frame, leeway = 70) { |
| 128 | + // Compare mouse's location to all four corners of a dead box: |
| 129 | + const offsets_from_dead_box = { |
| 130 | + plus : [this.mouse.from_center[ 0 ] + leeway, this.mouse.from_center[ 1 ] + leeway], |
| 131 | + minus: [this.mouse.from_center[ 0 ] - leeway, this.mouse.from_center[ 1 ] - leeway] |
| 132 | + }; |
| 133 | + // Apply a camera rotation movement, but only when the mouse is |
| 134 | + // past a minimum distance (leeway) from the canvas's center: |
| 135 | + if ( !this.look_around_locked) |
| 136 | + // If steering, steer according to "mouse_from_center" vector, but don't |
| 137 | + // start increasing until outside a leeway window from the center. |
| 138 | + for (let i = 0; i < 2; i++) { |
| 139 | + // The &&'s in the next line might zero the vectors out: |
| 140 | + let o = offsets_from_dead_box, |
| 141 | + velocity = ((o.minus[ i ] > 0 && o.minus[ i ]) || (o.plus[ i ] < 0 && o.plus[ i ])) * |
| 142 | + radians_per_frame; |
| 143 | + // On X step, rotate around Y axis, and vice versa. |
| 144 | + this.matrix ().post_multiply (Mat4.rotation (-velocity, i, 1 - i, 0)); |
| 145 | + this.inverse ().pre_multiply (Mat4.rotation (+velocity, i, 1 - i, 0)); |
| 146 | + } |
| 147 | + this.matrix ().post_multiply (Mat4.rotation (-.1 * this.roll, 0, 0, 1)); |
| 148 | + this.inverse ().pre_multiply (Mat4.rotation (+.1 * this.roll, 0, 0, 1)); |
| 149 | + // Now apply translation movement of the camera, in the newest local coordinate frame. |
| 150 | + this.matrix ().post_multiply (Mat4.translation (...this.thrust.times (-meters_per_frame))); |
| 151 | + this.inverse ().pre_multiply (Mat4.translation (...this.thrust.times (+meters_per_frame))); |
| 152 | + } |
| 153 | + third_person_arcball (radians_per_frame) { |
| 154 | + // Spin the scene around a point on an axis determined by user mouse drag: |
| 155 | + const dragging_vector = this.mouse.from_center.minus (this.mouse.anchor); |
| 156 | + if (dragging_vector.norm () <= 0) |
| 157 | + return; |
| 158 | + this.matrix ().post_multiply (Mat4.translation (0, 0, -25)); |
| 159 | + this.inverse ().pre_multiply (Mat4.translation (0, 0, +25)); |
| 160 | + |
| 161 | + const rotation = Mat4.rotation (radians_per_frame * dragging_vector.norm (), |
| 162 | + dragging_vector[ 1 ], dragging_vector[ 0 ], 0); |
| 163 | + this.matrix ().post_multiply (rotation); |
| 164 | + this.inverse ().pre_multiply (rotation); |
| 165 | + |
| 166 | + this.matrix ().post_multiply (Mat4.translation (0, 0, +25)); |
| 167 | + this.inverse ().pre_multiply (Mat4.translation (0, 0, -25)); |
| 168 | + } |
| 169 | + render_animation (context) { |
| 170 | + const m = this.speed_multiplier * this.meters_per_frame, |
| 171 | + r = this.speed_multiplier * this.radians_per_frame, |
| 172 | + dt = this.uniforms.animation_delta_time / 1000; |
| 173 | + |
| 174 | + // TODO: Once there is a way to test it, remove the below, because uniforms are no longer inaccessible |
| 175 | + // outside this function, so we could just tell this class to take over the uniforms' matrix anytime. |
| 176 | + if (this.will_take_over_uniforms) { |
| 177 | + this.reset (); |
| 178 | + this.will_take_over_uniforms = false; |
| 179 | + } |
| 180 | + // Move in first-person. Scale the normal camera aiming speed by dt for smoothness: |
| 181 | + this.first_person_flyaround (dt * r, dt * m); |
| 182 | + // Also apply third-person "arcball" camera mode if a mouse drag is occurring: |
| 183 | + if (this.mouse.anchor) |
| 184 | + this.third_person_arcball (dt * r); |
| 185 | + // Log some values: |
| 186 | + this.pos = this.inverse ().times (vec4 (0, 0, 0, 1)); |
| 187 | + this.z_axis = this.inverse ().times (vec4 (0, 0, 1, 0)); |
| 188 | + } |
| 189 | + }; |
0 commit comments