|
| 1 | +const FENCE_WIDTH = 15; |
| 2 | + |
| 3 | +/** |
| 4 | + * Non-finite values. |
| 5 | + * @param {*} x value |
| 6 | + * @param {number} fallback fallback |
| 7 | + * @returns {number} x or fallback |
| 8 | + */ |
| 9 | +const finite = (x, fallback) => (typeof x === 'number' && Number.isFinite(x) ? x : fallback); |
| 10 | + |
| 11 | +class CameraView { |
| 12 | + constructor (name) { |
| 13 | + this.name = name; |
| 14 | + this.x = 0; |
| 15 | + this.y = 0; |
| 16 | + this.dir = 0; |
| 17 | + this.scale = 1; |
| 18 | + this.a = 1; |
| 19 | + this.b = 0; |
| 20 | + this.active = false; |
| 21 | + this.drawables = new Set(); |
| 22 | + this._sweepAt = 64; |
| 23 | + } |
| 24 | + |
| 25 | + set (state) { |
| 26 | + this.x = finite(state && state.x, 0); |
| 27 | + this.y = finite(state && state.y, 0); |
| 28 | + this.dir = finite(state && state.dir, 0); |
| 29 | + this.scale = finite(state && state.scale, 1); |
| 30 | + const radians = this.dir * Math.PI / 180; |
| 31 | + this.a = this.scale * Math.cos(radians); |
| 32 | + this.b = this.scale * Math.sin(radians); |
| 33 | + this.active = !(this.x === 0 && this.y === 0 && this.dir === 0 && this.scale === 1); |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +/** |
| 38 | + * Premultiply a drawable's model matrix. |
| 39 | + * @param {Drawable} drawable the drawable, with an active `_ctPmCamera` |
| 40 | + * @param {Float32Array} m m |
| 41 | + */ |
| 42 | +const applyView = (drawable, m) => { |
| 43 | + const view = drawable._ctPmCamera; |
| 44 | + const a = view.a; |
| 45 | + const b = view.b; |
| 46 | + const m0 = m[0]; |
| 47 | + const m1 = m[1]; |
| 48 | + const m4 = m[4]; |
| 49 | + const m5 = m[5]; |
| 50 | + m[0] = (a * m0) - (b * m1); |
| 51 | + m[1] = (b * m0) + (a * m1); |
| 52 | + m[4] = (a * m4) - (b * m5); |
| 53 | + m[5] = (b * m4) + (a * m5); |
| 54 | + |
| 55 | + const px = drawable._position[0] - view.x; |
| 56 | + const py = drawable._position[1] - view.y; |
| 57 | + let sx = (a * px) - (b * py); |
| 58 | + let sy = (b * px) + (a * py); |
| 59 | + if (!drawable._highQuality) { |
| 60 | + sx = Math.round(sx); |
| 61 | + sy = Math.round(sy); |
| 62 | + } |
| 63 | + const ox = m[12] - drawable._position[0]; |
| 64 | + const oy = m[13] - drawable._position[1]; |
| 65 | + m[12] = (a * ox) - (b * oy) + sx; |
| 66 | + m[13] = (b * ox) + (a * oy) + sy; |
| 67 | +}; |
| 68 | + |
| 69 | +/** |
| 70 | + * The drawable's scale as seen on screen, for choosing texture resolution. |
| 71 | + * @param {Drawable} drawable the drawable, with an active `_ctPmCamera` |
| 72 | + * @returns {Array<number>} [x, y] scale in percent |
| 73 | + */ |
| 74 | +const viewScale = drawable => { |
| 75 | + const zoom = Math.abs(drawable._ctPmCamera.scale); |
| 76 | + return [drawable._scale[0] * zoom, drawable._scale[1] * zoom]; |
| 77 | +}; |
| 78 | + |
| 79 | +/** |
| 80 | + * @param {Drawable} drawable a drawable |
| 81 | + * @returns {boolean} true true |
| 82 | + */ |
| 83 | +const rotated = drawable => !!drawable._ctPmCamera && drawable._ctPmCamera.dir % 90 !== 0; |
| 84 | + |
| 85 | +/** |
| 86 | + * RenderWebGL#getFencedPositionOfDrawable for a drawable on a camera. |
| 87 | + * @param {RenderWebGL} renderer the renderer |
| 88 | + * @param {Drawable} drawable the drawable, with an active `_ctPmCamera` |
| 89 | + * @param {Array<number>} position requested world position |
| 90 | + * @param {Rectangle} fenceBounds scratch rectangle |
| 91 | + * @returns {Array<number>} fenced world position |
| 92 | + */ |
| 93 | +const fencedPosition = (renderer, drawable, position, fenceBounds) => { |
| 94 | + const view = drawable._ctPmCamera; |
| 95 | + const toScreen = (x, y) => { |
| 96 | + const dx = x - view.x; |
| 97 | + const dy = y - view.y; |
| 98 | + return [(view.a * dx) - (view.b * dy), (view.b * dx) + (view.a * dy)]; |
| 99 | + }; |
| 100 | + const target = toScreen(position[0], position[1]); |
| 101 | + const current = toScreen(drawable._position[0], drawable._position[1]); |
| 102 | + let x = target[0]; |
| 103 | + let y = target[1]; |
| 104 | + const dx = x - current[0]; |
| 105 | + const dy = y - current[1]; |
| 106 | + const aabb = drawable._skin.getFenceBounds(drawable, fenceBounds); |
| 107 | + const inset = Math.floor(Math.min(aabb.width, aabb.height) / 2); |
| 108 | + const sx = renderer._xRight - Math.min(FENCE_WIDTH, inset); |
| 109 | + if (aabb.right + dx < -sx) { |
| 110 | + x = Math.ceil(current[0] - (sx + aabb.right)); |
| 111 | + } else if (aabb.left + dx > sx) { |
| 112 | + x = Math.floor(current[0] + (sx - aabb.left)); |
| 113 | + } |
| 114 | + const sy = renderer._yTop - Math.min(FENCE_WIDTH, inset); |
| 115 | + if (aabb.top + dy < -sy) { |
| 116 | + y = Math.ceil(current[1] - (sy + aabb.top)); |
| 117 | + } else if (aabb.bottom + dy > sy) { |
| 118 | + y = Math.floor(current[1] + (sy - aabb.bottom)); |
| 119 | + } |
| 120 | + if (x === target[0] && y === target[1]) return [position[0], position[1]]; |
| 121 | + const det = (view.a * view.a) + (view.b * view.b); |
| 122 | + if (det === 0) return [position[0], position[1]]; |
| 123 | + return [ |
| 124 | + view.x + (((view.a * x) + (view.b * y)) / det), |
| 125 | + view.y + (((view.a * y) - (view.b * x)) / det) |
| 126 | + ]; |
| 127 | +}; |
| 128 | + |
| 129 | +/** |
| 130 | + * Point a drawable at the camera object it should use. |
| 131 | + * @param {RenderWebGL} renderer the renderer |
| 132 | + * @param {Drawable} drawable the drawable |
| 133 | + * @param {?CameraView} view its camera |
| 134 | + * @param {boolean} [force] force |
| 135 | + */ |
| 136 | +const attach = (renderer, drawable, view, force) => { |
| 137 | + const next = view && view.active ? view : null; |
| 138 | + if (drawable._ctPmCamera === next && !(force && next)) { |
| 139 | + return; |
| 140 | + } |
| 141 | + drawable._ctPmCamera = next; |
| 142 | + drawable.setTransformDirty(); |
| 143 | + renderer.dirty = true; |
| 144 | +}; |
| 145 | + |
| 146 | +const methods = { |
| 147 | + /** |
| 148 | + * @param {string} name camera name |
| 149 | + * @param {boolean} create make it if it doesn't exist |
| 150 | + * @returns {?CameraView} the camera |
| 151 | + * @private |
| 152 | + */ |
| 153 | + _ctPmCamera (name, create) { |
| 154 | + if (!this._ctPmCameras) { |
| 155 | + if (!create) return null; |
| 156 | + this._ctPmCameras = new Map(); |
| 157 | + } |
| 158 | + let view = this._ctPmCameras.get(name); |
| 159 | + if (!view && create) { |
| 160 | + view = new CameraView(name); |
| 161 | + this._ctPmCameras.set(name, view); |
| 162 | + } |
| 163 | + return view || null; |
| 164 | + }, |
| 165 | + |
| 166 | + /** |
| 167 | + * Set a camera. |
| 168 | + * @param {string} name camera name |
| 169 | + * @param {?{x: number, y: number, dir: number, scale: number}} state state |
| 170 | + */ |
| 171 | + ctPmSetCamera (name, state) { |
| 172 | + name = String(name); |
| 173 | + const view = this._ctPmCamera(name, !!state); |
| 174 | + if (!view) return; |
| 175 | + const wasActive = view.active; |
| 176 | + const before = [view.x, view.y, view.dir, view.scale]; |
| 177 | + view.set(state); |
| 178 | + if (!wasActive && !view.active) return; |
| 179 | + if (before[0] === view.x && before[1] === view.y && before[2] === view.dir && before[3] === view.scale) return; |
| 180 | + for (const id of view.drawables) { |
| 181 | + const drawable = this._allDrawables[id]; |
| 182 | + if (!drawable || drawable._ctPmCameraName !== name) { |
| 183 | + view.drawables.delete(id); |
| 184 | + continue; |
| 185 | + } |
| 186 | + attach(this, drawable, view, true); |
| 187 | + } |
| 188 | + this.dirty = true; |
| 189 | + }, |
| 190 | + |
| 191 | + /** |
| 192 | + * Bind a drawable to a camera. |
| 193 | + * @param {int} drawableID the drawable |
| 194 | + * @param {?string} name camera name |
| 195 | + */ |
| 196 | + ctPmSetDrawableCamera (drawableID, name) { |
| 197 | + const drawable = this._allDrawables[drawableID]; |
| 198 | + if (!drawable) return; |
| 199 | + name = (name === null || typeof name === 'undefined') ? null : String(name); |
| 200 | + const current = typeof drawable._ctPmCameraName === 'undefined' ? null : drawable._ctPmCameraName; |
| 201 | + if (current === name) return; |
| 202 | + if (current !== null) { |
| 203 | + const old = this._ctPmCamera(current, false); |
| 204 | + if (old) old.drawables.delete(drawableID); |
| 205 | + } |
| 206 | + drawable._ctPmCameraName = name; |
| 207 | + let view = null; |
| 208 | + if (name !== null) { |
| 209 | + view = this._ctPmCamera(name, true); |
| 210 | + view.drawables.add(drawableID); |
| 211 | + if (view.drawables.size >= view._sweepAt) { |
| 212 | + for (const id of view.drawables) { |
| 213 | + const d = this._allDrawables[id]; |
| 214 | + if (!d || d._ctPmCameraName !== name) view.drawables.delete(id); |
| 215 | + } |
| 216 | + view._sweepAt = Math.max(64, view.drawables.size * 2); |
| 217 | + } |
| 218 | + } |
| 219 | + attach(this, drawable, view, false); |
| 220 | + }, |
| 221 | + |
| 222 | + /** |
| 223 | + * @param {string} name camera name |
| 224 | + * @returns {{x: number, y: number, dir: number, scale: number}} the camera as currently drawn |
| 225 | + */ |
| 226 | + ctPmGetCamera (name) { |
| 227 | + const view = this._ctPmCamera(String(name), false); |
| 228 | + if (!view) return {x: 0, y: 0, dir: 0, scale: 1}; |
| 229 | + return {x: view.x, y: view.y, dir: view.dir, scale: view.scale}; |
| 230 | + } |
| 231 | +}; |
| 232 | + |
| 233 | +/** |
| 234 | + * @param {Function} RenderWebGL RenderWebGL |
| 235 | + */ |
| 236 | +const install = RenderWebGL => { |
| 237 | + for (const name of Object.keys(methods)) { |
| 238 | + RenderWebGL.prototype[name] = methods[name]; |
| 239 | + } |
| 240 | +}; |
| 241 | + |
| 242 | +module.exports = { |
| 243 | + install, |
| 244 | + applyView, |
| 245 | + viewScale, |
| 246 | + rotated, |
| 247 | + fencedPosition, |
| 248 | + CameraView |
| 249 | +}; |
0 commit comments