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
1,468 lines (1,265 loc) • 72.6 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.*/
"use strict"
define.class('$system/base/node', function(require){
// Base UI view object
var FlexLayout = require('$system/lib/layout')
var Render = require('$system/base/render')
var Shader = this.Shader = require('$system/platform/$platform/shader$platform')
var view = this.constructor
this.attributes = {
// wether to draw it
visible: true,
drawtarget: Config({type:Enum('both','pick','color'), value:'both'}),
// pos(ition) of the view, relative to parent. For 2D only the first 2 components are used, for 3D all three.
pos: Config({type:vec3, value:vec3(0,0,0),meta:"xyz"}),
// alias for the x component of pos
x: Config({alias:'pos', index:0}),
// alias for the y component of pos
y: Config({alias:'pos', index:1}),
// alias for the z component of pos
z: Config({alias:'pos', index:2}),
// alias for the x component of pos
left: Config({alias:'pos', index:0}),
// alias for the y component of pos
top: Config({alias:'pos', index:1}),
// alias for the z component of pos
front: Config({alias:'pos', index:2}),
// the bottom/right/rear corner, used by layout
corner: Config({type:vec3, value:vec3(NaN)}),
// alias for the x component of corner
right: Config({alias:'corner', index:0}),
// alias for y component of corner
bottom: Config({alias:'corner', index:1}),
// alias for z component of corner
rear: Config({alias:'corner', index:2}),
// the background color of a view, referenced by various shaders
bgcolor: Config({group:"style", type:vec4, value: vec4(0,0,0,0), meta:"color"}),
// the background image of a view. Accepts a string-url or can be assigned a require('./mypic.png')
bgimage: Config({group:"style",type:Object, meta:"texture"}),
// the opacity of the image
opacity: Config({group:"style", value: 1.0, type:float}),
// Per channel color filter, each color is a value in the range 0.0 ~ 1.0 and is multiplied by the color of the background image
colorfilter: Config({group:"style", type:vec4, value: vec4(1,1,1,1), meta:"color"}),
// Image mode alters how/where the background image is scaled, streched, fit and drawn within the view's bounds.
bgimagemode: Config({group:"style", type:Enum("resize", "custom", "stretch", "aspect-fit", "aspect-fill", "center", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"), value:"stretch"}),
bgimageaspect: Config({group:"style", value:vec2(1,1)}),
// Offets the image within the view. This value is in texture coordinates.
bgimageoffset: Config({group:"style", value:vec2(0,0)}),
// When using `aspect-fit`, or `apsect-fill` this property will automatically adjust the image's location within the view.
bgimagealign: Config({group:"style", type:Enum("none", "center", "start", "end", "left", "right", "top", "bottom", "top-left", "top-right", "bottom-left", "bottom-right"), value:"none"}),
// the clear color of the view when it is in '2D' or '3D' viewport mode
clearcolor: Config({group:"style",type:vec4, value: NaN, meta:"color"}),
// the scroll position of the view matrix, allows to scroll/move items in a viewport. Only works on a viewport:'2D'
// this property is manipulated by the overflow:'SCROLL' scrollbars
scroll: Config({type:vec2, value:vec2(0, 0), persist: true}),
// the zoom factor of the view matrix, allows zooming of items in a viewport. Only works on viewport:'2D'
zoom: Config({type:float, value:1}),
// overflow control, shows scrollbars when the content is larger than the viewport. If any value is set, it defaults to viewport:'2D'
// works the same way as the CSS property
overflow: Config({type: Enum('','hidden','scroll','auto','hscroll','vscroll'), value:''}),
// size, this holds the width/height/depth of the view. When set to NaN it means the layout engine calculates the size
size: Config({type:vec3, value:vec3(NaN), meta:"xyz"}),
// internal, alias for the x component of size
w: Config({alias:'size', index:0}),
// internal, alias for the y component of size
h: Config({alias:'size', index:1}),
// internal, alias for the z component of size
d: Config({alias:'size', index:2}),
// alias for setting the x component of size. To read the width, use this.layout.width.
width: Config({alias:'size', index:0}),
// alias for the y component of size. To read the height, use this.layout.height.
height: Config({alias:'size', index:1}),
// alias for the z component of size
depth: Config({alias:'size', index:2}),
percentsize: Config({type:vec3, value:vec3(NaN)}),
// alias for the x component of percentsize
percentwidth: Config({alias:'percentsize', index:0}),
// alias for the y component of percentsize
percentheight: Config({alias:'percentsize', index:1}),
// alias for the z component of percentsize
percentdepth: Config({alias:'percentsize', index:2}),
percentpos: Config({type:vec3, value:vec3(NaN)}),
// internal, percentage widths/heights
percentx: Config({alias:'percentpos', index:0}),
// internal, percentage widths/heights
percenty: Config({alias:'percentpos', index:1}),
// internal, percentage widths/heights
percentz: Config({alias:'percentpos', index:2}),
// the pixelratio of a viewport. Allows scaling the texture buffer to arbitrary resolutions. Defaults to the system (low/high DPI)
pixelratio: Config({type: float, value:NaN}),
// the minimum size for the flexbox layout engine
minsize: Config({type: vec3, value:vec3(NaN), meta:"xyz"}),
// the maximum size for the flexbox layout engine
maxsize: Config({type: vec3, value:vec3(NaN), meta:"xyz"}),
// alias for the x component of minsize
minwidth: Config({alias:'minsize', index:0}),
// alias for the y component of minsize
minheight: Config({alias:'minsize', index:1}),
// alias for the z component of minsize
mindepth: Config({alias:'minsize', index:2}),
// alias for the x component of maxsize
maxwidth: Config({alias:'maxsize', index:0}),
// alias for the y component of maxsize
maxheight: Config({alias:'maxsize', index:1}),
// alias for the z component of maxsize
maxdepth: Config({alias:'maxsize', index:2}),
// the margin on 4 sides of the box (left, top, right, bottom). Can be assigned a single value to set them all at once
margin: Config({type: vec4, value: vec4(0,0,0,0), meta: "ltrb"}),
// alias for the first component of margin
marginleft: Config({alias:'margin', index:0}),
// alias for the second component of margin
margintop: Config({alias:'margin', index:1}),
// alias for the third component of margin
marginright: Config({alias:'margin', index:2}),
// alias for the fourth component of margin
marginbottom: Config({alias:'margin', index:3}),
// the padding on 4 sides of the box (left, top, right, bottom) Can be assigned a single value to set them all at once
padding: Config({type: vec4, value: vec4(0,0,0,0), meta: "ltrb"}),
// alias for the first component of padding
paddingleft: Config({alias:'padding', index:0}),
// alias for the second component of padding
paddingtop: Config({alias:'padding', index:1}),
// alias for the third component of padding
paddingright: Config({alias:'padding', index:2}),
// alias for the fourth component of padding
paddingbottom: Config({alias:'padding', index:3}),
translate: Config({type: vec3, value:vec3(0.)}),
// Scale of an item, only useful for items belof a 3D viewport
scale: Config({type: vec3, value: vec3(1), meta:"xyz"}),
// The anchor point around which items scale and rotate, depending on anchor mode its either a factor of size or and absolute value
anchor: Config({type: vec3, value: vec3(0.)}),
// the mode with which the anchor is computed. Factor uses the size of an item to find the point, defaulting to center
anchormode: Config({type:Enum('','factor','absolute'), value:'factor'}),
// rotate the item around x, y or z in radians. If you want degrees type it like this: 90*DEG
rotate: Config({type: vec3, value: vec3(0), meta:"xyz"}),
// the color of the border of an item.
bordercolor: Config({group:"style",type: vec4, value: vec4(0,0,0,0), meta:"color"}),
// the radius of the corners of an item, individually settable left, top, right, bottom. Setting this value will switch to rounded corner shaders
borderradius: Config({group:"style",type: vec4, value: vec4(0,0,0,0)}),
// the width of the border. Setting this value will automatically enable the border shaders
borderwidth: Config({group:"style",type: vec4, value: vec4(0,0,0,0)}),
// alias for the first component of borderwidth
borderleftwidth: Config({alias:'borderwidth', index:0}),
// alias for the second component of borderwith
bordertopwidth: Config({alias:'borderwidth', index:1}),
// alias for the third component of borderwith
borderrightwidth: Config({alias:'borderwidth', index:2}),
// alias for the fourth component of borderwith
borderbottomwidth: Config({alias:'borderwidth', index:3}),
// turn on flex sizing. Flex is a factor that distributes either the widths or the heights of nodes by this factor
// flexbox layout is a web standard and has many great tutorials online to learn how it works
flex: Config({group:"layout", type: float, value: NaN}),
// wraps nodes around when the flexspace is full
flexwrap: Config({group:"layout", type: Enum('wrap','nowrap'), value: "wrap"}),
// which direction the flex layout is working,
flexdirection: Config({group:"layout", type: Enum('row','column'), value: "row"}),
// pushes items eitehr to the start, center or end
justifycontent: Config({group:"layout", type: Enum('','flex-start','center','flex-end','space-between','space-around'), value: ""}),
// align items to either start, center, end or stretch them
alignitems: Config({group:"layout", type: Enum('flex-start','center','flex-end','stretch'), value:"stretch"}),
// overrides the parents alignitems with our own preference
alignself: Config({group:"layout", type: Enum('', 'flex-start','center','flex-end','stretch'), value:""}),
// item positioning, if absolute it steps 'outside' the normal flex layout
position: Config({group:"layout", type: Enum('relative','absolute'), value: "relative" }),
// the layout object, contains width/height/top/left after computing. Its a read-only property and should be used in shaders only.
// Can be listened to to observe layout changes
layout: Config({ type:Object, value:{}, meta:"hidden"}),
// When set to 2D or 3D the render engine will create a separate texture pass for this view and all its children
// using a 2D viewport is a great way to optimize render performance as when nothing changes, none of the childstructures
// need to be processed and a single texture can just be drawn by the parent
// the viewportblend shader can be used to render this texture it into its parent
viewport: Config({group:"layout", type:Enum('','2d','3d'), value:''}),
// the field of view of a 3D viewport. Only useful on a viewport:'3D'
fov: Config({group:"3d", type:float, value: 45}),
// the nearplane of a 3D viewport, controls at which Z value near clipping start. Only useful on a viewport:'3D'
nearplane: Config({group:"3d",type:float, value: 0.001}),
// the farplane of a 3D viewport, controls at which Z value far clipping start. Only useful on a viewport:'3D'
farplane: Config({group:"3d",type:float, value: 1000}),
// the position of the camera in 3D space. Only useful on a viewport:'3D'
camera: Config({group:"3d",type: vec3, value: vec3(-2,2,-2)}),
// the point the camera is looking at in 3D space. Only useful on a viewport:'3D'
lookat: Config({group:"3d",type: vec3, value: vec3(0)}),
// the up vector of the camera (which way is up for the camera). Only useful on a viewport:'3D'
up: Config({group:"3d",type: vec3, value: vec3(0,-1,0)}),
// internal, the current time which can be used in shaders to create continous animations
time:Config({meta:"hidden", value:0}),
// fires when pointer is pressed down.
pointerstart:Config({type:Event}),
// fires when pointer is pressed and moved (dragged).
pointermove:Config({type:Event}),
pointermultimove:Config({type:Event}),
// fires when pointer is released.
pointerend:Config({type:Event}),
// fires when pointer is pressed and released quickly.
pointertap:Config({type:Event}),
// fires when pointer moved without being pressed.
pointerhover:Config({type:Event}),
// fires when pointer enters an element.
pointerover:Config({type:Event}),
// fires when pointer leaves an element.
pointerout:Config({type:Event}),
// fires when mouse wheel is used.
pointerwheel:Config({type:Event}),
// fires when a drag drop item enters
dragover:Config({type:Event}),
// fires when a drag drop item moves
dragmove:Config({type:Event}),
// fires when a drag drop item leaves
dragout:Config({type:Event}),
// fires when a key goes to up. The event argument is {repeat:int, code:int, name:String}
keyup: Config({type:Event}),
// fires when a key goes to down. The event argument is {repeat:int, code:int, name:String}
keydown: Config({type:Event}),
// fires when a key gets pressed. The event argument is {repeat:int, value:string, char:int}
keypress: Config({type:Event}),
// fires when someone pastes data into the view. The event argument is {text:string}
keypaste: Config({type:Event}),
// fires when this view loses focus
blur: Config({type:Event}),
// drop shadow size
dropshadowradius:Config({type:float, value:20}),
// drop shadow movement
dropshadowoffset:Config({type:vec2, value:vec2(0,0)}),
// drop shadow hardness
dropshadowhardness:Config({type:float, value:0.5, minvalue: 0, maxvalue:1}),
// drop shadow opacity
dropshadowopacity:Config({type:float, value:0, minvalue: 0, maxvalue:1}),
// drop shadow color
dropshadowcolor:Config({type:vec4,meta:"color", value:vec4("black")}),
// whether this view has focus
focus: Config({meta:"hidden", value:false, persist:true}),
// tabstop, sorted by number
tabstop: NaN,
// the type of pointer cursor to use for this view
cursor: Config({type:Enum(
'', 'arrow', 'none','wait','text','pointer',
'zoom-in','zoom-out','grab','grabbing',
'ns-resize','ew-resize','nwse-resize','nesw-resize',
'w-resize','e-resize','n-resize','s-resize',
'nw-resize','ne-resize','sw-resize','se-resize',
'help','crosshair','move',
'col-resize','row-resize',
'vertical-text','context-menu','no-drop','not-allowed',
'alias','cell','copy'
), value:''}),
// The number of render passes for this view. Note that corresponding
// inner classes will need to be created for multi pass rendering to work,
// see /classes/ui/blurtest.js for an example.
passes: Config({type:int, value:0, minvalue: 0, maxvalue:10}),
}
this.name = ""
this.class = ""
this.oncamera = this.onlookat = this.onup = function(){
this.redraw()
}
this.onopacity = function(opacity){
// set/unset this.visible
if (opacity < 0.002) {
if (this._visible) {
this.visible = false
this.__opacityhidvisible = true
}
} else {
if (this.__opacityhidvisible && (! this._visible)) {
this.visible = true
}
}
}
this.onvisible = function(visible){
this.__opacityhidvisible = false
if (visible && this._opacity >= 0.002) this.redraw()
}
// the number of pick ID's to reserve for this view.
this.pickrange = 1
this.boundscheck = true
// the local matrix
this.modelmatrix = mat4.identity()
// the concatenation of all parent model matrices
this.totalmatrix = mat4.identity()
// the last view matrix used
this.viewmatrix = mat4.identity()
// the viewport matrix used to render the viewportblend
this.viewportmatrix = mat4.identity()
// the normal matrix contains the transform without translate (for normals)
this.normalmatrix = mat4.identity()
// the remap matrix used to remap pointer vec2 to local space
this.remapmatrix = mat4()
// forward references for shaders
this.layout = {width:0, height:0, left:-1, top:-1, right:0, bottom:0}
this.screen = {device:{size:vec2(), frame:{size:vec2()}}}
this.noise = require('$system/shaderlib/noiselib')
this.pal = require('$system/shaderlib/palettelib')
this.shape = require('$system/shaderlib/shapelib')
this.math = require('$system/shaderlib/mathlib')
this.demo = require('$system/shaderlib/demolib')
this.material = require('$system/shaderlib/materiallib')
this.colorlib = require('$system/shaderlib/colorlib')
// turn off rpc proxy generation for this prototype level
this.rpcproxy = false
this.ondropshadowradius = function(){
if (this.dropshadowopacity > 0){
this.shadowrect = true
} else {
this.shadowrect = false
}
}
// internal, listen to switch the shaders when borderradius changes
this.onborderradius = function(){
this.setBorderShaders()
}
this.onborderwidth = function(){
this.setBorderShaders()
}
this.onbgcolor = function(){
this.setBorderShaders()
}
this.setBorderShaders = function(){
var radius = this._borderradius
//var value = event.value
var border_on = true
var width = this._borderwidth
if (width[0] === 0 && width[1] === 0 && width[2] === 0 && width[3] === 0){
border_on = false
} else {
border_on = true
}
var bg_on = isNaN(this._bgcolor[0])? false: true
if (this._viewport === '3d') border_on = false, bg_on = false
if (radius[0] !== 0 || radius[1] !== 0 || radius[2] !== 0 || radius[3] !== 0){
// this switches the bg shader to the rounded one
if (this._bgimage){
this.roundedimage = true
this.roundedrect = false
} else {
this.roundedrect = bg_on
this.roundedimage = false
}
this.roundedborder = border_on
this.hardimage = false
this.hardrect = false
this.hardborder = false
} else {
if (this._bgimage){
this.hardimage = true
this.hardrect = false
} else {
this.hardrect = bg_on
this.hardimage = false
}
this.hardborder = border_on
this.roundedrect = false
this.roundedborder = false
this.roundedimage = false
}
}
// internal, listen to the viewport to turn off our background and border shaders when 3D
this.onviewport = function(event){
this.setBorderShaders()
}
// internal, automatically turn a viewport:'2D' on when we have an overflow (scrollbars) set
this.onoverflow = function(){
if (this._overflow){
if (!this._viewport) this._viewport = '2d'
}
}
// internal, setting focus to true
this.onfocus = function(event){
if (!event.mark){ // someone set it to true that wasnt us
this.screen.setFocus(this)
}
}
// internal, put a tablistener
this.ontabstop = function(event){
if (isNaN(event.old) && !isNaN(event.value)){
this.addListener('keydown', function(value){
if (value.name === 'tab'){
if (value.shift) {
this.screen.focusPrev(this)
} else {
this.screen.focusNext(this)
}
}
})
}
}
// draw dirty is a bitmask of 2 bits, the guid-dirty and the color-dirty
this.draw_dirty = 3
// layout dirty causes a relayout to occur (only on viewports)
this.layout_dirty = true
// update dirty causes a shader update to occur
this.update_dirty = true
// update matrix stack
this.matrix_dirty = true
// internal, initialization of a view
this.atViewInit = function(prev){
this.anims = {}
//this.layout = {width:0, height:0, left:0, top:0, right:0, bottom:0}
this.shader_list = []
this.initialized = true
if (prev){
this.modelmatrix = prev.modelmatrix
this.totalmatrix = prev.totalmatrix
this.viewportmatrix = prev.viewportmatrix
this.layout = prev.layout
} else {
this.modelmatrix = mat4()
if (this._viewport) {
this.totalmatrix = mat4.identity()
} else {
this.totalmatrix = mat4()
}
this.viewportmatrix = mat4()
}
// create shaders
this.shaders = {}
for (var key in this.shader_enable){
var enable = this.shader_enable[key]
if (!enable) continue
var shader = this[key]
if (shader){
var prevshader = prev && prev.shaders && prev.shaders[key]
var shobj
// ok so instead of comparing constructor, lets compare the computational result
if (prevshader && (prevshader.constructor === shader || prevshader.isShaderEqual(shader.prototype, this, prev))){
shobj = prevshader
Object.defineProperty(shobj, 'constructor',{value:shader, configurable:true})
shobj.view = this
shobj.outer = this
// ok now check if we need to dirty it
if (shobj._view_listeners) for (var shkey in shobj._view_listeners){
this.addListener(shkey, shobj.reupdate.bind(shobj))
var value = this[shkey]
if (!(value && value.struct && value.struct.equals(value, prev[shkey]) || value === prev[shkey])){
shobj.reupdate(shkey)
}
}
prevshader.reused = true
} else {
shobj = new shader(this)
}
shobj.shadername = key
this.shaders[key] = shobj
this.shader_list.push(shobj)
}
}
if (this._bgimage){
this.onbgimage()
}
// a _viewport is a top-level canvas object, or a scrolling view.
if (this._viewport){
for (var key in this.shaders){
var shader = this.shaders[key]
if (shader.dont_scroll_as_viewport){
this.shaders[key].noscroll = true
}
}
this.shaders.viewportblend = new this.viewportblend(this)
if (this.passes > 0) {
// loop and create RenderPass instances
for (var i = 0; i < this.passes; i++) {
// based on this.passes with names pass0..9
var key = 'pass' + i
if (key in this) {
this.shaders[key] = new this[key](this)
} else {
console.warn('you are missing an inner class named', key, 'in', this)
}
}
}
}
this.sortShaders()
}
// lets destroy some shaders/vertexbuffers
this.atViewDestroy = function(){
for (var key in this.shaders){
var shader = this.shaders[key]
if (!shader.reused && shader.hasOwnProperty('mesh') && !shader.mesh.sticky){
this.screen.device.gl.deleteBuffer(shader.mesh.glvb)
shader.mesh.glvb = undefined
}
shader.reused = undefined
}
}
Object.defineProperty(this, 'bg', {
get:function(){},
set:function(){
console.error('bg property depricated, please use bgcolor:NaN to turn off background shader, and subclass via bgcolorfn or hardrect/roundedrect specifically')
}
})
this.onbgimage = function(){
if (this.initialized){
if (typeof this._bgimage === 'string'){
// Path to image was specified
if (require.loaded(this._bgimage)){
var img = require(this._bgimage)
this.setBgImage(img)
} else {
// check if loaded already
require.async(this._bgimage, 'jpeg').then(function(result){
this.setBgImage(result)
}.bind(this))
}
} else {
this.setBgImage(this._bgimage)
}
} else {
this.setBorderShaders()
}
}
this.defaultKeyboardHandler = function(v, prefix){
if (!prefix) prefix = ""
var keyboard = this.screen.keyboard
keyboard.textarea.focus()
var name = prefix + 'keydown' + v.name[0].toUpperCase() + v.name.slice(1)
//this.undo_group++
if (keyboard.leftmeta || keyboard.rightmeta) name += 'Cmd'
if (keyboard.ctrl) name += 'Ctrl'
if (keyboard.alt) name += 'Alt'
if (keyboard.shift) name += 'Shift'
if (this[name]) {
this[name](v)
} else if (this.keydownHandler) {
this.keydownHandler(name)
}
}
// image can be an image, or a Texture (has array property).
this.setBgImage = function(image){
var shader = this.shaders.hardimage || this.shaders.roundedimage
if (!shader) return
// Callback method to update the bgimage. Some platforms support a
// second argument to Texture.fromImage for delayed loading
var update = function(img) {
if (!img) return
shader.texture = img
if (this.bgimagemode === "resize"){
this._size = img.size
this.relayout()
} else if (img) {
this.onbgimagemode()
} else this.redraw()
}.bind(this)
update((image.array) ? image : Shader.Texture.fromImage(image, update))
}
// internal, emit an event upward (to all parents) untill a listener is hit
this.emitUpward = function(key, msg){
if (this['_listen_'+key] || this['on'+key]){
this.emit(key, msg)
return this
}
if (this.parent) {
return this.parent.emitUpward(key, msg)
}
}
this.findEmitUpward = function(key){
if (this['_listen_'+key] || this['on'+key]){
return this
}
if (this.parent) {
return this.parent.findEmitUpward(key)
}
}
this.computeCursor = function(){
var node = this
while(node){
if (node._cursor !== ''){
this.screen.pointer.cursor = node._cursor
break
}
node = node.parent
}
}
// internal, sorts the shaders
this.sortShaders = function(){
var shaders = this.shaders
this.shader_draw_list = this.shader_list.slice(0).sort(function(a, b){
return shaders[a.shadername].draworder > shaders[b.shadername].draworder
}.bind(this))
this.shader_update_list = this.shader_list.slice(0).sort(function(a, b){
return shaders[a.shadername].updateorder > shaders[b.shadername].updateorder
}.bind(this))
}
// internal, custom hook in the inner class assignment to handle nested shaders specifically
this.atInnerClassAssign = function(key, value){
if (!this.hasOwnProperty('shader_enable')) this.shader_enable = Object.create(this.shader_enable || {})
// set the shader order
if (!value || typeof value === 'number' || typeof value === 'boolean'){
this.shader_enable[key] = value? true: false
return
}
// its a class assignment
if (typeof value === 'function' && Object.getPrototypeOf(value.prototype) !== Object.prototype){
this['_' + key] = value
return
}
// its inheritance
var cls = this['_' + key]
this['_' + key] = cls.extend(value, this)
}
// internal, redraw our view and bubble up the viewport dirtiness to the root
this.redraw = function(){
if (!this.parent_viewport){
return
}
if (this.parent_viewport.draw_dirty === 3){
return
}
var parent = this
while(parent){
var viewport = parent.parent_viewport
if (!viewport) break
if (viewport.draw_dirty === 3){
return
}
viewport.draw_dirty = 3
parent = viewport.parent
}
this.draw_dirty = 3
if (this.screen.device && this.screen.device.redraw) {
this.screen.device.redraw()
}
}
// internal, updates all the shaders
this.reupdate = function(){
var shaders = this.shader_list
if (shaders) for (var i = 0; i < shaders.length; i++){
shaders[i].reupdate()
}
}
this.getViewGuid = function(){
if (this.viewguid) return this.viewguid
if (this.pickguid){
this.viewguid = '' +this.pickguid
}
var node = this, id = ''
while(node){
if (node.parent) id += node.parent.children.indexOf(node)
node = node.parent
}
this.viewguid = id
return id
}
function UnProject(glx, gly, glz, modelview, projection){
var inv = vec4()
var A = mat4.mat4_mul_mat4(modelview, projection)
var m = mat4.invert(A)
inv[0] = glx
inv[1] = gly
inv[2] = 2.0 * glz - 1.0
inv[3] = 1.0
out = vec4.vec4_mul_mat4(inv, m)
// divide by W to perform perspective!
out[0] /= out[3]
out[1] /= out[3]
out[2] /= out[3]
return vec3(out)
}
// Internal: remap the x and y coordinates to local space
this.globalToLocal = function(value){
//TODO(aki): Simplify.
var sx = this.screen.device.main_frame.size[0] / this.screen.device.ratio
var sy = this.screen.device.main_frame.size[1] / this.screen.device.ratio
var mx = value[0] / (sx / 2) - 1.0
var my = -1 * (value[1] / (sy / 2) - 1.0)
var parentlist = [], ip = this.parent
while (ip){
if (ip._viewport || !ip.parent) parentlist.push(ip)
ip = ip.parent
}
var raystart = vec3(mx,my,-100)
var rayend = vec3(mx,my,100)
var lastrayafteradjust = vec3(mx,my,-100)
var lastprojection = mat4.identity()
var lastviewmatrix = mat4.identity()
var camerapos = vec3(0)
var scaletemp = mat4.scalematrix([1,1,1])
var transtemp2 = mat4.translatematrix([-1,-1,0])
var lastmode = "2d"
this.remapmatrix = mat4.identity()
for (var i = parentlist.length - 1; i >= 0; i--) {
var P = parentlist[i]
var newmode = P.parent? P._viewport:"2d"
if (P.parent) {
var MM = P._viewport? P.viewportmatrix: P.totalmatrix
if (!P.viewportmatrix) console.log(i, "whaaa" )
mat4.invert(P.viewportmatrix, this.remapmatrix)
// 3d to layer transition -> do a raypick.
if (lastmode == "3d") {
var startv = UnProject(lastrayafteradjust.x, lastrayafteradjust.y, 0, lastviewmatrix, lastprojection)
var endv = UnProject(lastrayafteradjust.x, lastrayafteradjust.y, 1, lastviewmatrix, lastprojection)
camlocal = vec3.mul_mat4(camerapos, this.remapmatrix)
endlocal = vec3.mul_mat4(endv, this.remapmatrix)
var R = vec3.intersectplane(camlocal, endlocal, vec3(0,0,-1), 0)
if (!R) {
raystart = vec3(0.5,0.5,0)
} else {
R = vec3.mul_mat4(R, P.viewportmatrix)
raystart = R
}
}
raystart = vec3.mul_mat4(raystart, this.remapmatrix)
mat4.scalematrix([P.layout.width/2,P.layout.height/2,1000/2], scaletemp)
mat4.invert(scaletemp, this.remapmatrix)
raystart = vec3.mul_mat4(raystart, this.remapmatrix)
raystart = vec3.mul_mat4(raystart, transtemp2)
lastrayafteradjust = vec3(raystart.x, raystart.y,-1)
lastprojection = P.colormatrices.perspectivematrix
lastviewmatrix = P.colormatrices.lookatmatrix
camerapos = P._camera
}
if (i == 0 && this.noscroll){
mat4.invert(P.colormatrices.noscrollmatrix, this.remapmatrix) } else {
mat4.invert(P.colormatrices.viewmatrix, this.remapmatrix)
}
raystart = vec3.mul_mat4(raystart, this.remapmatrix)
lastmode = newmode
}
var MM = this._viewport?this.viewportmatrix: this.totalmatrix
mat4.invert(MM, this.remapmatrix)
raystart = vec3.mul_mat4(raystart, this.remapmatrix)
rayend = vec3.mul_mat4(rayend, this.remapmatrix)
return vec2(raystart.x, raystart.y)
}
// internal, this gets called by the render engine
this.updateShaders = function(){
if (!this.update_dirty) return
this.update_dirty = false
// we can wire up the shader
if (!this._shaderswired){
this.atAttributeGet = function(attrname){
//if (this.constructor.name === 'label')
//console.log(this.constructor.name, attrname, this['_'+attrname])
// monitor attribute wires for geometry
// lets add a listener
if (!shader._view_listeners) shader._view_listeners = {}
shader._view_listeners[attrname] = 1
this.addListener(attrname,shader.reupdate.bind(shader, attrname))
}.bind(this)
}
var shaders = this.shader_update_list
for (var i = 0; i < shaders.length; i ++){
var shader = shaders[i]
if (shader.update && shader.update_dirty){
shader.update_dirty = false
shader.update()
}
}
if (!this._shaderswired) {
this._shaderswired = true
this.atAttributeGet = undefined
}
}
// starts a drag view via render function
this.startDrag = function(pointerevent, render){
var dragview = this.screen.openOverlay(render)
if (!dragview.atDragMove){
dragview.atDragMove = function(position){
this.x = position[0] - this.width*0.5
this.y = position[1] - this.height*0.5
}
}
// make sure we pick the screen in pointermove
pointerevent.pickview = true
dragview.atDragMove(pointerevent.value)
var lastdrag
this.onpointermove = function(event){
dragview.atDragMove(event.value)
// lets send dragenter/leave events
var newdrag = event.pick
if (!dragview.isDropTarget(newdrag,event)) newdrag = undefined
if (lastdrag !== newdrag){
if (lastdrag) lastdrag.emitUpward('dragout',{})
if (newdrag) newdrag.emitUpward('dragover',{})
lastdrag = newdrag
}
if (newdrag) newdrag.emitUpward('dragmove', event)
}
this.onpointerend = function(event){
this.onpointermove = undefined
dragview.closeOverlay()
if (lastdrag){
lastdrag.emitUpward('dragout',{})
}
dragview.atDrop(lastdrag, event)
this.onpointerend = undefined
}
}
// internal, decide to inject scrollbars into our childarray
this.atRender = function(){
if (this._viewport === '2d' && (this._overflow === 'auto' || this._overflow === 'scroll' || this._overflow === 'hscroll' || this._overflow === 'vscroll' || this._overflow === 'auto')){
if (this.vscrollbar) this.vscrollbar.value = 0
if (this.hscrollbar) this.hscrollbar.value = 0
this.scroll = function(event){
if (event.mark) return
if (this.vscrollbar){
this.vscrollbar.value = Mark(event.value[1])
}
if (this.hscrollbar){
this.hscrollbar.value = Mark(event.value[0])
}
}
if (this._overflow === 'auto' || this._overflow === 'scroll' || this._overflow === 'vscroll') this.children.push(
this.vscrollbar = this.scrollbar({
position:'absolute',
vertical:true,
noscroll:true,
value:function(event){
if (event.mark) return
this.parent.scroll = Mark(vec2(this.parent._scroll[0],this._value))
},
layout:function(){
var parent_layout = this.parent.layout
var this_layout = this.layout
this_layout.top = 0
this_layout.width = 10
this_layout.height = parent_layout.height
this_layout.left = parent_layout.width - this_layout.width
}
})
)
if (this._overflow === 'auto' || this._overflow === 'scroll' || this._overflow === 'hscroll') this.children.push(
this.hscrollbar = this.scrollbar({
position: 'absolute',
vertical: false,
noscroll: true,
value: function(event){
if (event.mark) return
this.parent.scroll = Mark(vec2(this._value,this.parent._scroll[1]))
},
layout: function(){
var parent_layout = this.parent.layout
var this_layout = this.layout
this_layout.left = 0
this_layout.height = 10
this_layout.width = parent_layout.width
this_layout.top = parent_layout.height - this_layout.height
}
})
)
if (this.hscrollbar) this.hscrollbar.value = Mark(this._scroll[0])
if (this.vscrollbar) this.vscrollbar.value = Mark(this._scroll[1])
this.pointerwheel = function(event){
// cumulative damped wheel value.
// Used to decide if enough horizontal/vertical movement is present.
this.dampedwheel = this.dampedwheel || vec2(0, 0, 0)
this.dampedwheel = vec2(
(this.dampedwheel[0] * 9 + event.wheel[0]) / 10,
(this.dampedwheel[1] * 9 + event.wheel[1]) / 10
)
if (this.vscrollbar && this.vscrollbar._visible){
if (!this.hscrollbar || !this.hscrollbar._visible) {
if (abs(this.dampedwheel[0]) > abs(this.dampedwheel[1]) * 2) return
}
this.vscrollbar.value = clamp(this.vscrollbar._value + event.wheel[1], 0, this.vscrollbar._total - this.vscrollbar._page)
}
if (this.hscrollbar && this.hscrollbar._visible){
if (!this.vscrollbar || !this.vscrollbar._visible) {
if (abs(this.dampedwheel[1]) > abs(this.dampedwheel[0]) / 2) return
}
this.hscrollbar.value = clamp(this.hscrollbar._value + event.wheel[0], 0, this.hscrollbar._total - this.hscrollbar._page)
}
}
//TODO(aki): implement zoom
// this.pointerzoom = function(event){
// var zoom = event.value.zoom
// var lastzoom = this._zoom
// var newzoom = clamp(lastzoom * (1+0.03 * zoom),0.01,10)
// this.zoom = newzoom
//
// var pos = this.globalToLocal(event.value.position)
//
// var shiftx = pos[0] * lastzoom - pos[0] * this._zoom
// var shifty = pos[1] * lastzoom - pos[1] * this._zoom
//
// this.hscrollbar.value = clamp(this.hscrollbar._value + shiftx, 0, this.hscrollbar._total - this.hscrollbar._page)
// this.vscrollbar.value = clamp(this.vscrollbar._value + shifty, 0, this.vscrollbar._total - this.vscrollbar._page)
//
// this.updateScrollbars()
// this.redraw()
// }
}
}
// internal, show/hide scrollbars
this.updateScrollbars = function(){
if (this.vscrollbar){
var scroll = this.vscrollbar
var totalsize = Math.floor(this.layout.boundh)
var viewsize = Math.floor(this.layout.height * this.zoom)
if (totalsize > viewsize+1){
scroll._visible = true
scroll._total = totalsize
scroll._page = viewsize
var off = clamp(scroll._value,0, scroll._total - scroll._page)
if (off !== scroll._value) scroll.value = off
} else {
if (0 !== scroll._offset){
scroll.value = 0
}
scroll._visible = false
}
}
if (this.hscrollbar){
var scroll = this.hscrollbar
var totalsize = Math.floor(this._layout.boundw)
var viewsize = Math.floor(this._layout.width* this.zoom)
if (totalsize > viewsize + 1){
scroll._visible = true
scroll._total = totalsize
scroll._page = viewsize
var off = clamp(scroll._value,0, scroll._total - scroll._page)
if (off !== scroll._value) scroll.value = off
} else {
if (0 !== scroll._value) scroll.value = 0
scroll._visible = false
}
}
}
// internal, called by doLayout, to update the matrices to layout and parent matrix
this.updateMatrices = function(parentmatrix, parentviewport, parent_changed, boundsinput, bailbound){
if (this.__isinvisible()) {
// console.log('skip updateMatrices', this)
return
}
// allow pre-matrix gen hooking
if (this.atMatrix) this.atMatrix()
var boundsobj = boundsinput
if (!boundsinput){
boundsobj = this._layout
boundsobj.absx = 0
boundsobj.absy = 0
boundsobj.boundw = 0
boundsobj.boundh = 0
}
var layout = this._layout
if (this.measured_width !== undefined || this.measured_height !== undefined){
var width = layout.absx + max(layout.width ,this.measured_width)
var height = layout.absy + max(layout.height, this.measured_height)
} else {
var width = layout.absx + layout.width
var height = layout.absy + layout.height
}
if (width > boundsobj.boundw) {
boundsobj.boundw = width
}
if (height > boundsobj.boundh) {
boundsobj.boundh = height
}
if (bailbound) return
var matrix_changed = parent_changed
if (parentviewport === '3d'){
matrix_changed = true
if (this._scale && this._anchor && this._rotate && this._pos){
mat4.TSRT2(this._anchor, this._scale, this._rotate, this._pos, this.modelmatrix)
} else {
mat4.identity(this.modelmatrix)
}
} else {
// compute TSRT matrix
if (layout){
//console.log(this.matrix_dirty)
//var ml = this.matrix_layout
//if (!ml || ml.left !== layout.left || ml.top !== layout.top ||
// ml.width !== layout.width || ml.height !== layout.height
// || ml.scale !== this._scale || ml.rotate !== this._rotate
//){
// this.matrix_layout = {
// left:layout.left,
/// top:layout.top,
// width:layout.width,
// height:layout.height,
// scale: this._scale,
// rotate:this._rotate
// }
matrix_changed = true
var s = this._scale
var r = this._rotate
var tr = this._translate
var t0 = layout.left + tr[0], t1 = layout.top+ tr[1], t2 = tr[2]
//if (this.name === 'handle') console.log(this.constructor.name, layout.top)
//var hw = ( this.layout.width !== undefined ? this.layout.width: this._size[0] ) / 2
//var hh = ( this.layout.height !== undefined ? this.layout.height: this._size[1]) / 2
var hw = layout.width / 2
var hh = layout.height / 2
mat4.TSRT(-hw, -hh, 0, s[0], s[1], s[2], r[0], r[1], r[2], t0 + hw * s[0], t1 + hh * s[1], t2, this.modelmatrix)
} else {
matrix_changed = true
var s = this._scale
var r = this._rotate
var t = this._translate
var hw = this._size[0] / 2
var hh = this._size[1] / 2
mat4.TSRT(-hw, -hh, 0, s[0], s[1], s[2], 0, 0, r[2], t[0] + hw * s[0], t[1] + hh * s[1], t[2], this.modelmatrix)
}
}
var parentmode = parentviewport
if (this._viewport){
if (parentmatrix) {
mat4.mat4_mul_mat4(this.modelmatrix, parentmatrix, this.viewportmatrix)
} else {
this.viewportmatrix = this.modelmatrix
}
mat4.identity(this.totalmatrix)
parentmode = this._viewport
parentmatrix = mat4.global_identity
} else {
if (parentmatrix && matrix_changed) mat4.mat4_mul_mat4( this.modelmatrix, parentmatrix, this.totalmatrix)
}
var children = this.children
var len = children.length
if (children) for (var i = 0; i < len; i++){
var child = children[i]
var clayout = child.layout
if (!clayout) continue
clayout.absx = layout.absx + clayout.left
clayout.absy = layout.absy + clayout.top
child.updateMatrices(this.totalmatrix, parentmode, matrix_changed, boundsobj, child._viewport)
}
if (!boundsinput){
this.updateScrollbars()
}
this.matrix_dirty = false
}
// emit post layout
function emitPostLayout(node, nochild){
var ref = node.ref
var layout = ref._layout
if (!nochild){
var children = node.children
for (var i = 0; i < children.length; i++){
var child = children[i]
emitPostLayout(child, child.ref._viewport)
}
ref.layout_dirty = false
}
var oldlayout = ref.oldlayout
if ((ref._listen_layout || ref.onlayout) && ref.oldlayout && (layout.left !== oldlayout.left || layout.top !== oldlayout.top ||
layout.width !== oldlayout.width || layout.height !== oldlayout.height)){
ref.emit('layout', {type:'setter', owner:ref, key:'layout', value:layout})
}
ref.oldlayout = layout
ref.matrix_dirty = true
if (ref._bgimage){
ref.onbgimagemode()
}
}
// cause this node, all childnodes and relevant parent nodes to relayout
this.relayoutRecur = function(source){
this.layout_dirty = true
this.draw_dirty = 3 // bitmask, 2 = pick, 1= color
for (var i = 0; i < this.child_viewport_list.length; i++){
var child = this.child_viewport_list[i]
//if (child._overflow) continue
if (child !== source){
child.relayoutRecur()
}
}
if (this.parent_viewport !== this){
if (this.parent_viewport._overflow) return
this.parent_viewport.relayoutRecur(this)
}
}
// ok so. what we need to do is
// scan up towards overflow something
// scan down and skip overflow something.
this.relayout = function(shallow){
if (this.layout_dirty) return
this.layout_dirty = true
this.redraw()
if (this.parent_viewport) this.parent_viewport.relayoutRecur(this)
}
this.rematrix = function(){
if (this.matrix_dirty) return
this.matrix_dirty = true
if (this.parent_viewport){
this.parent_viewport.matrix_dirty = true
this.redraw()
}
}
// internal, moving a position in absolute should only trigger a matrix reload
this.pos = function(pos){
if (this._layout.left === this._pos[0] && this._layout.top === this._pos[1]) {
return
}
if (this._position === 'absolute'){
this._layout.left = this._pos[0]
this._layout.top = this._pos[1]
this.rematrix()
} else {
this.relayout()
}
}
this.corner =
this.size =
this.minsize =
this.maxsize =
this.margin =
this.padding =
this.flex =
this.flexwrap =
this.flexdirection =
this.justifycontent =
this.alignitems =
this.alignself =
this.position =
this.relayout
this.translate =
this.scale =
this.rotate = this.rematrix
this.__isinvisible = function() {
var view = this
while (view) {
if (view._visible === false) {
return true
}
view = view.parent
}
}
// internal, called by the render engine
this.doLayout = function(){
var layout = this._layout
var size = this._size
if (this.parent && !isNaN(this._flex)){ // means our layout has been externally defined
var flex = this._flex
var presizex = isNaN(this._percentsize[0])?layout.width:this.parent._layout.width * this._percentsize[0]
var presizey = isNaN(this._percentsize[1])?layout.height: this.parent._layout.height * this._percentsize[1]
//console.log(this._percentsize, presizex,presizey)
//this._size = vec2(layout.width, layout.height)
var flexwrap = this._flexwrap
this._flex = 1
this._size = vec2(presizex, presizey)
this._flexwrap = false
// implemented by text and label
if (this.measure) this.measure() // otherwise it doesnt get called
var copynodes = FlexLayout.fillNodes(this)
FlexLayout.computeLayout(copynodes)
this._flex = flex
this._size = size
this._flexwrap = flexwrap
this._layout = layout
emitPostLayout(copynodes)
} else {
var pos = this._pos
var presizex = isNaN(this._percentsize[0])?size[0]: this.parent._layout.width * 0.01 * this._percentsize[0]
var presizey = isNaN(this._percentsize[1])?size[1]: this.parent._layout.height * 0.01 * this._percentsize[1]
var preposx = isNaN(this._percentpos[0])?pos[0]: this.parent._layout.width * 0.01 * this._percentpos[0]
var preposy = isNaN(this._percentpos[1])?pos[1]: this.parent._layout.height * 0.01 * this._percentpos[1]
// we have some kind of overflow, cause we are a viewport
// so if height is not given we have to take up the external height
//console.log(presizey, this.layout.height)
//console.log("This is where we get the percentage size", this._percentsize, presizex,presizey)
this._size = vec2(presizex, presizey)
this._pos = vec2(preposx, preposy)
var preheight = this._layout.height
var copynodes = FlexLayout.fillNodes(this)
FlexLayout.computeLayout(copynodes)
if (isNaN(presizey)){
this._layout.height = preheight
}
this._size = size
this._pos = pos
emitPostLayout(copynodes)
}
}
// Animates an attribute over time.
// <attribute> {String} The name of the attribute to animate on this view.
// <track> {Object} An object consisting of keys with time offset (in seconds)/value pairs. Each value can be discrete, or an object with motion and value keys where motion describes the interpolation from the previous value to this one, and value describes the value to animate to.
// Returns a promise that resolves when the animation completes. This allows animations
// to be chained together, or other behaviors to occur when an animation ends.
this.animate = function(attribute, track){
return new define.Promise(function(resolve, reject){
this.startAnimation(attribute, undefined, track, resolve)
}.bind(this))
}
// internal, called by animation setters
this.startAnimation = function(attribute, value, track, resolve){
if(this.hasListenerProp(attribute, 'name', '__atAttributeGetRender')){
console.error('WARNING: Animating property "' + attribute + '", which can cause performance problems. Update your class to listen for animation updates and set state there instead of in render(): ', this)
}
if (this.initialized) {
return this.screen.startAnimationRoot(this, attribute, value, track, resolve)
} else {
return false
}
}
// Stops a running animation for an attribute
// <attribute> {String} The name of the attribute to stop animating on this view.
this.stopAnimation = function(attribute){
if (this.initialized) this.screen.stopAnimationRoot(this, attribute)
}
// Determines the background color that should be drawn at a given position.
// Returns a vec4 color value, defaults to bgcolor.
this.bgcolorfn = function(pos /*vec2*/){
return bgcolor
}
this.appendChild = function(render){
// wrap our render function in a temporary view
var vroot = view()
// set up a temporary view
vroot.render = render
vroot.parent = this
vroot.rpc = this.rpc
vroot.screen = this.screen
vroot.parent_viewport = this._viewport?this:this.parent_viewport
// render it
Render.process(vroot, undefined, undefined, true)
// move the children over
this.children.push.apply(this.children, vroot.children)
for (var i = 0; i < vroot.children.length; i++){
vroot.children[i].parent = this
}
// lets cause a relayout
this.relayout()
}
define.class(this, 'hardrect', this.Shader, function(){
this.updateorder = 0
this.draworder = 0
this.dont_scroll_as_viewport = true
this.mesh = vec2.array()
this.mesh.pushQuad(0,0,1,0,0,1,1,1)
this.position = function(){
uv = mesh.xy
pos = vec2(mesh.x * view.layout.width, mesh.y * view.layout.height)
var res = vec4(pos, -0.004, 1) * view.totalmatrix * view.viewmatrix
return res
}
this.color = function(){
var col = view.bgcolorfn(mesh.xy)
return vec4(col.rgb, col.a * view.opacity)
}
})
this.hardrect = true
this.bordercolorfn = function(pos){
return bordercolor
}
define.class(this, 'hardborder', this.Shader, function(){
this.updateorder = 0
this.draworder = 1
this.mesh = vec2.array()
this.dont_scroll_as_viewport = true
this.update = function(){
var view = this.view
var width = view.layout?view.layout.width:view.width
var height = view.layout?view.layout.height:view.height
var bw1 = view.borderwidth[0]/width
var bw2 = view.borderwidth[1]/width
var bw3 = view.borderwidth[2]/height
var bw4 = view.borderwidth[3]/height
var mesh = this.mesh = vec2.array()
mesh.pushQuad(0,0, bw1,0,0,1,bw1,1)
mesh.pushQuad(1-bw2,0, 1,0,1-bw2,1,1,1)
mesh.pushQuad(0,0, 1,0,0,bw3,1,bw3)
mesh.pushQuad(0,1-bw4, 1,1-bw4,0,1,1,1)
}
this.mesh.pushQuad(0,0,1,0,0,1,1,1)
this.mesh.pushQuad(0,0,1,0,0,1,1,1)
this.mesh.pushQuad(0,0,1,0,0,1,1,1)
this.mesh.pushQuad(0,0,1,0,0,1,1,1)
this.position = function(){
uv = mesh.xy
pos = vec2(mesh.x * view.layout.width, mesh.y * view.layout.height)
return vec4(pos, 0, 1) * view.totalmatrix * view.viewmatrix
}
this.color = func