dreemgl
Version:
DreemGL is an open-source multi-screen prototyping framework for mediated environments, with a visual editor and shader styling for webGL and DALi runtimes written in JavaScript. As a toolkit for gpu-accelerated multiscreen development, DreemGL includes
353 lines (314 loc) • 11.8 kB
JavaScript
/* DreemGL is a collaboration between Teeming Society & Samsung Electronics, sponsored by Samsung and others.
Copyright 2015-2016 Teeming Society. Licensed under the Apache License, Version 2.0 (the "License"); You may not use this file except in compliance with the License.
You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.*/
// Pointer emits events that unify mouse and touch interactions.
define.class('$system/base/node', function(){
var TAPSPEED = 150
var TAPDIST = 5
this.atConstructor = function(){}
// Internal: Pointer list with helper methods.
var PointerList = function () {
Array.call(this)
}
PointerList.prototype = Object.create( Array.prototype )
PointerList.prototype.constructor = PointerList
// Internal: Finds first unused id in sorted pointer list
PointerList.prototype.getAvailableId = function () {
var id = 0
for (var i = 0; i < this.length; i++) {
if (this[i].id === id) id += 1
}
return id
}
// Internal: Finds closest pointer ID in the list to a specified pointer
PointerList.prototype.getClosest = function (pointer) {
var closest, dist, closestdist = Infinity
for (var i = 0; i < this.length; i++) {
dist = vec2.distance(pointer.position, this[i].position)
if (dist < closestdist) {
closest = this[i]
closestdist = dist
}
}
return closest !== undefined ? closest : undefined
}
// Internal: Returns a pointer with specified ID
PointerList.prototype.getById = function (id) {
for (var i = 0; i < this.length; i++) {
if (this[i].id === id) {
return this[i]
}
}
}
// Internal: Removes specified pointer from the list
PointerList.prototype.removePointer = function (pointer) {
for (var i = this.length; i--;) {
if (this[i].id === pointer.id) {
this.splice(i, 1)
return
}
}
}
// Internal: Adds pointer to the list or replaces it if ID match is found
PointerList.prototype.setPointer = function (pointer) {
for (var i = this.length; i--;) {
if (this[i].id === pointer.id) {
this.splice(i, 1, new Pointer(pointer, pointer.id, pointer.view))
return
}
}
this.push(new Pointer(pointer, pointer.id, pointer.view))
this.sort(function(a, b) {
if (a.id < b.id) return -1
if (a.id > b.id) return 1
return 0
})
}
// Internal: Iterates over all views associated with the pointer list and
// calls specified callback with the view and filtered pointers as arguments
PointerList.prototype.forEachView = function (callback) {
var views = []
for (var i = 0; i < this.length; i++) {
if (this[i].view && views.indexOf(this[i].view) == -1) {
views.push(this[i].view)
}
}
for (var i = 0; i < views.length; i++) {
var pointers = []
for (var j = 0; j < this.length; j++) {
if (this[j].view === views[i] && pointers.indexOf(this[j]) == -1) {
pointers.push(this[j])
}
}
callback(views[i], pointers)
}
}
// Internal: Returns pointer object.
// It calculats deltas, min and max is reference pointer is provided.
var Pointer = function(pointer, id, view) {
// TODO(aki): add start value
this.id = id
this.view = view
this.value = pointer.value
this.position = pointer.position
this.button = pointer.button
this.shift = pointer.shift
this.alt = pointer.alt
this.ctrl = pointer.ctrl
this.meta = pointer.meta
this.touch = pointer.touch
this.delta = pointer.delta || vec2()
this.min = pointer.min || vec2()
this.max = pointer.max || vec2()
this.dt = pointer.dt || 0
this.movement = pointer.movement || vec2()
this.isover = pointer.isover
this.pick = pointer.pick
this.clicker = pointer.clicker
this.t = Date.now()
if (pointer.wheel !== undefined) this.wheel = pointer.wheel
}
Pointer.prototype.addDelta = function(refpointer) {
this.delta = vec2(this.position[0] - refpointer.position[0], this.position[1] - refpointer.position[1])
this.min = vec2(min(this.position[0], refpointer.position[0]), min(this.position[1], refpointer.position[1]))
this.max = vec2(max(this.position[0], refpointer.position[0]), max(this.position[1], refpointer.position[1]))
this.dt = this.t - refpointer.t
}
Pointer.prototype.addMovement = function(refpointer) {
this.movement = vec2(this.position[0] - refpointer.position[0], this.position[1] - refpointer.position[1])
}
// Adds click count to pointer
Pointer.prototype.setClicker = function(list) {
this.clicker = 0
for (var i = 0; i < list.length; i++) {
if (this.t - list[i].t < TAPSPEED * (i + 1)) this.clicker += 1
}
}
// TODO(aki): initialize per instance
this.attributes = {
// List of pointers that are captured.
first:Config({type: Array, value: new PointerList()}),
// List of pointers at the moment of capture.
start:Config({type: Array, value: new PointerList()}),
// List of captured pointers while moving.
move:Config({type: Array, value: new PointerList()}),
// List of pointers released from capture.
end:Config({type: Array, value: new PointerList()}),
// List of pointers that satisfy tap criteria at the moment of release.
tap:Config({type: Array, value: new PointerList()}),
// List of uncaptured pointers in movinf state (should apply only to mouse).
hover:Config({type: Array, value: new PointerList()}),
// List of pointers that entered a view.
over:Config({type: Array, value: new PointerList()}),
// List of pointers that exited a view.
out:Config({type: Array, value: new PointerList()}),
// List of pointers that emitted wheel event (should apply only to mouse).
wheel:Config({type: Array, value: new PointerList()}),
// list of previous taps
clickerstash:Config({type: Array, value: []})
}
this.emitPointerList = function(pointerlist, eventname) {
pointerlist.forEachView(function(view, pointers) {
pointers.forEach(function(pointer) {
// delete pointer.view
this.emit(eventname, {view: view, pointer: pointer})
}.bind(this))
this.emit(eventname, {view: view, pointers: pointers})
}.bind(this))
}
// Internal: emits `start` event.
// TODO(aki): down with button 2 seems to trigger end and tap. Investigate.
this.setstart = function(pointerlist) {
// scan for handoff hooks on in flight pointers
for (var i = 0; i < this._start.length; i++) {
var start = this._start[i]
if (start.atHandOver){
var id = start.atHandOver(pointerlist)
if (id >= 0){
// we got a handoff of a particular pointer
pointerlist[id].handovered = start.view
}
}
}
this._start.length = 0
var pick = function(view){
var id = this._first.getAvailableId()
var pointer = new Pointer(pointerlist[i], id, view)
// Add pointer to clicker stash for counting
this._clickerstash.unshift(pointer)
this._clickerstash.length = min(this._clickerstash.length, 5)
pointer.setClicker(this._clickerstash)
// set pointer lists
this._start.setPointer(pointer)
this._first.setPointer(pointer)
this._move.setPointer(pointer)
}.bind(this)
for (var i = 0; i < pointerlist.length; i++) {
// if a pointer is handoffed use that view instead
if (pointerlist[i].handovered) pick(pointerlist[i].handovered)
else this.device.pickScreen(pointerlist[i].position, pick, true)
}
this.emitPointerList(this._start, 'start')
}
// Internal: emits `move` event.
this.setmove = function(pointerlist) {
this._wheel.length = 0
for (var i = 0; i < pointerlist.length; i++) {
var previous = this._move.getClosest(pointerlist[i])
if(!previous){
console.log('pointer lost!')
continue
}
var start = this._start.getById(previous.id)
var first = this._first.getById(previous.id)
var pointer = new Pointer(pointerlist[i], previous.id, first.view)
pointer.addDelta(first)
pointer.addMovement(previous || first)
// emit event hooks
if (start){
if (start.pickview){
this.device.pickScreen(pointerlist[i].position, function(view){
pointer.pick = view
}.bind(this), true)
}
if (start.atMove) start.atMove(pointerlist[i], pointerlist[i].value, start)
}
this._move.setPointer(pointer)
}
this.emitPointerList(this._move, 'move')
}
// Internal: emits `end` event.
// Emits `tap` event if conditions are met.
this.setend = function(pointerlist) {
this._end.length = 0
this._tap.length = 0
for (var i = 0; i < pointerlist.length; i++) {
var closest = this._move.getClosest(pointerlist[i])
if(!closest){
console.log('pointer lost!')
continue
}
// emit event hooks
var start = this._start.getById(closest.id)
if (start){
if (start.atEnd) start.atEnd(pointerlist[i], pointerlist[i].value, start)
}
this.device.pickScreen(pointerlist[i].position, function(view){
var previous = this._move.getClosest(pointerlist[i])
var first = this._first.getById(previous.id)
var pointer = new Pointer(pointerlist[i], previous.id, first.view)
pointer.addDelta(first)
pointer.setClicker(this._clickerstash)
pointer.isover = pointer.view === view
this._first.removePointer(first)
this._end.setPointer(pointer)
this._move.removePointer(pointer)
if (pointer.dt < TAPSPEED && vec2.len(pointer.delta) < TAPDIST){
this._tap.setPointer(pointer)
}
}.bind(this), true)
}
this.emitPointerList(this._end, 'end')
this.emitPointerList(this._tap, 'tap')
}
// Internal: emits `hover` event.
this.sethover = function(pointerlist) {
this._over.length = 0
this._out.length = 0
this.device.pickScreen(pointerlist[0].position, function(view){
var previous = this._hover.getById(0)
if (previous) previous = new Pointer(previous, 0, previous.view)
var pointer = new Pointer(pointerlist[0], 0, view)
this._hover.setPointer(pointer)
// TODO(aki): entering child view triggers out event. Consider adding pointer-events: 'none'
if (!previous || previous.view !== pointer.view) {
if (pointer.view) {
this._over.setPointer(pointer)
}
if (previous) {
this._out.setPointer(previous)
}
}
// TODO(aki): fix hover, over, out and drag API
this.emitPointerList(this._hover, 'hover')
this.emitPointerList(this._over, 'over')
this.emitPointerList(this._out, 'out')
}.bind(this))
}
// TODO(aki): implement over/out on touch start/end
// Internal: emits `over` event.
this.setover = function() {}
// TODO(aki): implement over/out on touch start/end
// Internal: emits `out` event.
this.setout = function() {}
// Internal: emits `wheel` event.
this.setwheel = function(pointerlist) {
var dist = Infinity
// Hack to prevent screen picking when mouse is not moving
if (this._wheel[0]) {
dist = vec2.distance(pointerlist[0].position, this._wheel[0].position)
}
if (dist > 0) {
if (this._start[0]) {
// Prevent continuous picking when dragging.
// TODO(aki): optimize the mousewheel picking below.
var pointer = new Pointer(pointerlist[0], 0, this._start[0].view)
pointer.value = pointer.wheel
this._wheel.setPointer(pointer)
} else {
this.device.pickScreen(pointerlist[0].position, function(view){
var pointer = new Pointer(pointerlist[0], 0, view)
pointer.value = pointer.wheel
this._wheel.setPointer(pointer)
}.bind(this), true)
}
} else {
var pointer = new Pointer(pointerlist[0], 0, this._wheel[0].view)
pointer.value = pointer.wheel
this._wheel.setPointer(pointer)
}
this.emitPointerList(this._wheel, 'wheel')
}
})