Browse Source

First tech demo

master
Sven Slootweg 6 years ago
parent
commit
7a2a3d89d8
  1. 3
      .gitignore
  2. 36
      README.md
  3. 6
      bin/www
  4. 146
      gulpfile.js
  5. 1
      index.coffee
  6. 11
      lib/apply-property-map.coffee
  7. 5
      lib/distance-from.coffee
  8. 31
      lib/dom-wait.coffee
  9. 102
      lib/html-renderer.coffee
  10. 186
      lib/index.coffee
  11. 87
      lib/layer-panel.coffee
  12. 106
      lib/layers.coffee
  13. 96
      lib/object-panel.coffee
  14. 17
      lib/object-type-image.coffee
  15. 32
      lib/object-type-text.coffee
  16. 101
      lib/property-panel.coffee
  17. 72
      lib/scene-panel.coffee
  18. 11
      lib/split-filter.coffee
  19. 39
      package.json
  20. 23
      test-inheritance.coffee
  21. 89
      test/style.css
  22. 168
      test/style.scss
  23. 10955
      test/test-bundle.js
  24. 1
      test/test.html
  25. 42
      test/test.jade
  26. 20
      test/test.js

3
.gitignore

@ -2,4 +2,5 @@
# https://help.github.com/articles/ignoring-files
# Example .gitignore files: https://github.com/github/gitignore
/bower_components/
/node_modules/
/node_modules/
/TODO

36
README.md

@ -0,0 +1,36 @@
# authored
A modular framework for building authoring tools.
Originally developed for [Adivix](https://github.com/Adivix), but usable for any kind of (2D) authoring tool project.
## Demo
[Here.](http://cryto.net/~joepie91/authored-test/test/test.html) No performance optimizations carried out yet, and very rough around the edges still, so Chrome is probably a better choice.
## Current state
__Not__ production-ready. Still under development. The plugins will eventually be put into their own repository.
## Plugins
* Core (stage, scenes, objects - not actually a plugin)
* Layers
* Object type - text
* Object type - image
## UI plugins
* Scene panel
* Layer panel
* Object panel
* Properties panel
* HTML renderer
## Utilities
* `dom-wait`; queues a function call for when the DOM has loaded, where necessary.
* `apply-property-map`; utility for mapping values to DOM element properties.
* `split-filter`; like `.filter`, but returns both a matching and non-matching array.
* `distance-from`; calculates direct distance between two points, used for the drag threshold in the HTML renderer.

6
bin/www

@ -0,0 +1,6 @@
#!/usr/bin/env node
st = require("st");
http = require("http");
http.createServer(st({path: process.cwd(), cache: false})).listen(5555)

146
gulpfile.js

@ -0,0 +1,146 @@
var gulp = require('gulp');
/* CoffeeScript compile deps */
var path = require('path');
var rename = require('gulp-rename');
var livereload = require('gulp-livereload');
var webpack = require("gulp-webpack");
var sass = require("gulp-sass");
var gutil = require('gulp-util');
var cache = require('gulp-cached');
var remember = require('gulp-remember');
var plumber = require('gulp-plumber');
var nodemon = require("gulp-nodemon");
var jade = require("gulp-jade");
var net = require("net");
var spy = require("through2-spy");
function namedLog(name) {
return function gutilLog(log) {
gc = gutil.colors;
items = ["[- " + gc.magenta(name) + " -]"];
for(i in arguments) {
items.push(arguments[i]);
}
gutil.log.apply(null, items);
}
}
tasks = {
jade: [
{
name: "test",
source: ["./test/test.jade"],
base: "./test/",
destination: "./test/"
}
],
sass: [
{
name: "test",
source: ["./test/style.scss"],
base: "./test/",
destination: "./test/"
}
]
}
function concatName(type, name) {
return type + "-" + name;
}
for (var type in tasks) {
var subTasks = tasks[type];
for (var i in subTasks) {
var subTask = subTasks[i];
(function(type, subTask) {
var taskName = concatName(type, subTask.name);
gulp.task(taskName, function() {
var processor;
switch(type) {
case "jade":
processor = jade(/*{locals: require("./templateUtil")}*/);
break;
case "sass":
processor = sass();
break;
}
return gulp.src(subTask.source, {base: subTask.base})
.pipe(plumber())
.pipe(cache(taskName))
.pipe(processor.on('error', gutil.log))
.pipe(spy.obj(namedLog(taskName)))
.pipe(remember(taskName))
.pipe(gulp.dest(subTask.destination));
});
})(type, subTask);
}
}
function checkServerUp(){
setTimeout(function(){
var sock = new net.Socket();
sock.setTimeout(50);
sock.on("connect", function(){
console.log("Triggering page reload...");
livereload.changed("*");
sock.destroy();
})
.on("timeout", checkServerUp)
.on("error", checkServerUp)
.connect(5555);
}, 70);
}
var startupTasks = [];
var watchTasks = [];
for (var type in tasks) {
var subTasks = tasks[type];
for (var i in subTasks) {
var subTask = subTasks[i];
var taskName = concatName(type, subTask.name);
startupTasks.push(taskName);
watchTasks.push([subTask, taskName]);
}
}
gulp.task('webpack', function(){
return gulp.src("./test/test.js")
.pipe(webpack({
watch: true,
module: {
loaders: [{ test: /\.coffee$/, loader: "coffee-loader" }]
},
resolve: { extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"] }
}))
.pipe(rename("test-bundle.js"))
.pipe(gulp.dest("./test/"));
});
gulp.task('watch', function () {
global.isWatching = true;
livereload.listen();
gulp.watch(['./test/*.js', './test/*.html', './test/*.css']).on('change', livereload.changed);
for (i in watchTasks) {
task = watchTasks[i];
gulp.watch(task[0].source, [task[1]]);
}
nodemon({script: "./bin/www", ext: "js", delay: 500}).on("start", checkServerUp);
});
startupTasks.push("webpack", "watch");
gulp.task('default', startupTasks);

1
index.coffee

@ -0,0 +1 @@
module.exports = require "./lib"

11
lib/apply-property-map.coffee

@ -0,0 +1,11 @@
module.exports = (map, object, domElement) ->
Object.keys(map).forEach (key) ->
if typeof map[key] == "function"
valueFunc = map[key]
object.on "changed:#{key}", (val) ->
[targetName, actualValue] = valueFunc(val)
domElement.css targetName, actualValue
else
object.on "changed:#{key}", (val) ->
domElement.css map[key], val

5
lib/distance-from.coffee

@ -0,0 +1,5 @@
module.exports = (x1, y1, x2, y2) ->
deltaX = Math.abs(x2 - x1)
deltaY = Math.abs(y2 - y1)
return Math.sqrt((deltaX ** 2) + (deltaY ** 2))

31
lib/dom-wait.coffee

@ -0,0 +1,31 @@
$ = require "jquery"
domInitialized = false
queue = []
$ ->
domInitialized = true
queue.forEach ([thisArg, func, args]) ->
console.log args
func.apply(thisArg, args)
queue = []
domWait = (thisArg, func, args) ->
if domInitialized
func.apply(thisArg, args)
else
queue.push [thisArg, func, args]
domWait.func = (func) ->
return ->
args = []
# This is needed to avoid killing the optimizer.
for i in [0...arguments.length]
args[i] = arguments[i]
domWait(this, func, args)
module.exports = domWait

102
lib/html-renderer.coffee

@ -0,0 +1,102 @@
$ = require "jquery"
applyPropertyMap = require "./apply-property-map"
distanceFrom = require "./distance-from"
moveThreshold = 5
textPropertyMap =
fontFamily: "fontFamily"
fontSize: (val) -> ["fontSize", "#{val}px"]
fontColor: "color"
bold: (val) -> console.log("bold", val); ["fontWeight", (if val then "bold" else "normal")]
italic: (val) -> ["fontStyle", (if val then "italic" else "normal")]
attachRenderer = (jqObject) ->
renderer =
stage: this
jqObject: jqObject
console.log renderer.stage
renderer.stage.on "scene:added", (scene) ->
scene.on "object:created", (object) ->
if object.type == "text"
domElement = $("<div></div>")
object.on "changed:text", (text) ->
domElement.text(text)
object.on "changed:html", (html) ->
domElement.html(html)
applyPropertyMap textPropertyMap, object, domElement
else if object.type == "image"
domElement = $("<img>")
object.on "changed:source", (source) ->
domElement.attr "src", source
domElement.appendTo jqObject
domElement.addClass "rendererObject"
domElement.data "uuid", object.uuid
domElement.data "scene-uuid", scene.uuid
object.domElement = domElement
domElement.on "mousedown", (event) ->
scene.setActiveObject(object.uuid)
event.preventDefault()
event.stopPropagation()
element = $(this)
dragging = false
startPosition = x: event.pageX, y: event.pageY
startOffset = x: (event.pageX - element.offset().left), y: (event.pageY - element.offset().top)
$(document).one "mouseup", (event) ->
$(document).off "mousemove.htmlRenderer"
$(document).on "mousemove.htmlRenderer", (event) ->
if distanceFrom(startPosition.x, startPosition.y, event.pageX, event.pageY) >= moveThreshold
dragging = true
if dragging
# FIXME: Set properties on the object instead
object.x = (event.pageX - startOffset.x)
object.y = (event.pageY - startOffset.y)
object.on "changed:x", (x) ->
domElement.offset left: x
object.on "changed:y", (y) ->
domElement.offset top: y
scene.on "object:switched", (object) ->
renderer.jqObject
.find ".rendererObject"
.removeClass "activated"
.filter (i, obj) -> ($(obj).data("uuid") == object.uuid)
.addClass "activated"
scene.on "object:removed", (object) ->
object.domElement.remove()
renderer.stage.on "scene:switched", (scene) ->
renderer.jqObject
.find ".rendererObject"
.hide()
.filter (i, obj) -> $(obj).data("scene-uuid") == scene.uuid
.show()
return renderer
API = (stage) ->
return {
attach: attachRenderer.bind(stage)
}
API.meta =
name: "htmlRenderer"
module.exports = API

186
lib/index.coffee

@ -0,0 +1,186 @@
uuid = require "uuid"
EventEmitter = require("events").EventEmitter
splitFilter = require "../lib/split-filter"
sceneNameIncrement = 1
objectNameIncrement = 1
class AuthoredObject extends EventEmitter
constructor: (@scene, options = {}) ->
@uuid = uuid.v4()
@_props = {}
@propMeta = {}
@registerInternalProperty "visible", true
@registerInternalProperty "editorVisible", true
@registerInternalProperty "editorFaded", false
if not options.name?
name = "Object #{objectNameIncrement++}"
else
name = options.name
@registerInternalProperty "name", name
@registerInternalProperty "type"
@registerProperty "x", type: "numeric"
@registerProperty "y", type: "numeric"
if options.type?
@type = options.type
registerInternalProperty: (propName, defaultValue) ->
@registerProperty(propName, null)
if defaultValue?
this[propName] = defaultValue
registerProperty: (propName, propMeta) ->
Object.defineProperty this, propName,
enumerable: true
get: => @_props[propName]
set: (value) =>
@_props[propName] = value
@emit "changed:#{propName}", @_props[propName], this
@emit "changed", propName, @_props[propName], this
# Explicitly allow specifying `null` for propMeta to avoid propMeta; this is used for internal properties.
if propMeta == undefined
@propMeta[propName] = {}
else if propMeta != null
@propMeta[propName] = propMeta
class AuthoredScene extends EventEmitter
constructor: (@stage, options = {}) ->
@uuid = uuid.v4()
@objects = {}
@orderedObjects = []
@activeObject = null
@_ignoredProperties = ["orderedObjects"]
if not options.name?
@name = "Scene #{sceneNameIncrement++}"
else
@name = options.name
_startAddObject: (object) ->
@objects[object.uuid] = object
@orderedObjects.push object
object.scene = this
_endAddObject: (object) ->
@emit "object:added", object
setName: (name) ->
@name = name
createObject: (options = {}) ->
newObject = new AuthoredObject(this, options)
@_startAddObject(newObject)
if options.autoActivate ? true
process.nextTick =>
@setActiveObject newObject.uuid
@emit "object:created", newObject
@_endAddObject(newObject)
addObject: (object) ->
@_startAddObject(object)
@_endAddObject(object)
removeObject: (uuid) ->
objectIndex = @orderedObjects.indexOf(@objects[uuid])
delete @objects[uuid]
[@orderedObjects, removedObjects] = splitFilter @orderedObjects, (object) ->
object.uuid != uuid
removedObjects.forEach (object) =>
@emit "object:removed", object
if objectIndex == 0
newActiveIndex = 0
else
newActiveIndex = objectIndex - 1
@setActiveObject(@orderedObjects[newActiveIndex]?.uuid)
getActiveObject: ->
return @objects[@activeObject]
setActiveObject: (uuid) ->
@activeObject = uuid
@emit "object:switched", @objects[uuid]
getObjectNameIncrement: ->
return objectNameIncrement++
module.exports = class AuthoredStage extends EventEmitter
constructor: (options = {}) ->
@plugins = []
@scenes = {}
@orderedScenes = []
@activeScene = null
process.nextTick =>
# Initial scene
@createScene()
use: (plugin) ->
pluginMeta = plugin.meta
# Verify that all dependencies exist...
if Array.isArray pluginMeta.dependencies
pluginMeta.dependencies.forEach (dep) =>
if not @plugins[dep]?
# FIXME: DependencyError
throw new Error "Unmet dependency: `#{pluginMeta.name}` requires `#{dep}`"
else if typeof pluginMeta.dependencies == "function"
pluginMeta.dependencies(this)
pluginAPI = plugin(this)
# In case of misbehaving plugins...
pluginAPI ?= {}
@plugins[pluginMeta.name] = pluginAPI
createScene: (options = {}) ->
newScene = new AuthoredScene(this, options)
@scenes[newScene.uuid] = newScene
@orderedScenes.push newScene
if options.autoActivate ? true
process.nextTick =>
@setActiveScene newScene.uuid
@emit "scene:added", newScene
removeScene: (uuid) ->
sceneIndex = @orderedScenes.indexOf(@scenes[uuid])
delete @scenes[uuid]
[@orderedScenes, removedScenes] = splitFilter @orderedScenes, (scene) ->
scene.uuid != uuid
removedScenes.forEach (scene) =>
@emit "scene:removed", scene
if sceneIndex == 0
newActiveIndex = 0
else
newActiveIndex = sceneIndex - 1
@setActiveScene(@orderedScenes[newActiveIndex]?.uuid)
getActiveScene: ->
return @scenes[@activeScene]
setActiveScene: (uuid) ->
@activeScene = uuid
@emit "scene:switched", @scenes[uuid]

87
lib/layer-panel.coffee

@ -0,0 +1,87 @@
$ = require "jquery"
domWait = require "../lib/dom-wait"
# Layer panel functions
createLayerItem = (uuid, name, sceneUuid) ->
list = @jqObject.find "ul.layerList"
$("<li></li>")
.appendTo list
.data "uuid", uuid
.data "scene-uuid", sceneUuid
.text name
removeLayerItem = (uuid) ->
@jqObject
.find "ul.layerList li"
.filter (i, li) -> $(li).data("uuid") == uuid
.remove()
switchLayerItem = (uuid) ->
@jqObject
.find "ul.layerList li"
.removeClass "selected"
.filter (i, li) -> $(li).data("uuid") == uuid
.addClass "selected"
# API methods
attachLayerPanel = (jqObject) ->
panel =
stage: this
jqObject: jqObject
createLayerItem: domWait.func(createLayerItem)
removeLayerItem: domWait.func(removeLayerItem)
switchLayerItem: domWait.func(switchLayerItem)
panel.stage.on "scene:added", (scene) ->
scene.on "layer:added", (layer) ->
panel.createLayerItem(layer.uuid, layer.name, scene.uuid)
# FIXME: For now assumes that we automatically activate the new layer.
panel.switchLayerItem(layer.uuid)
scene.on "layer:removed", (layer) ->
panel.removeLayerItem(layer.uuid)
scene.on "layer:switched", (layer) ->
if layer?
panel.switchLayerItem(layer.uuid)
panel.stage.on "scene:switched", (scene) ->
jqObject
.find "ul.layerList li"
.hide()
.filter (i, li) -> $(li).data("scene-uuid") == scene.uuid
.show()
if activeLayer = scene.getActiveLayer?()
panel.switchLayerItem activeLayer.uuid
jqObject
.find ".action-addLayer"
.on "click", (event) ->
activeScene = panel.stage.getActiveScene()
activeScene.createLayer()
jqObject
.find ".action-removeLayer"
.on "click", (event) ->
targetUuid = panel.stage.getActiveScene().getActiveLayer().uuid
panel.stage.getActiveScene().removeLayer targetUuid
jqObject
.find "ul.layerList"
.on "mousedown", "li", (event) ->
targetUuid = $(this).data "uuid"
panel.stage.getActiveScene().setActiveLayer targetUuid
# Public API
API = (stage) ->
return {
attach: attachLayerPanel.bind(stage)
}
API.meta =
name: "layerPanel"
dependencies: ["layers"]
module.exports = API

106
lib/layers.coffee

@ -0,0 +1,106 @@
uuid = require "uuid"
splitFilter = require "../lib/split-filter"
nameIncrement = 1
# Scene methods
createLayer = (options = {}) ->
if not options.name?
name = "Layer #{nameIncrement++}"
else
name = options.name
newUuid = uuid.v4()
layerObject =
name: name
uuid: newUuid
scene: this
@layers[newUuid] = layerObject
@orderedLayers.push layerObject
if options.autoActivate ? true
@setActiveLayer(newUuid)
@emit "layer:added", layerObject
return layerObject
removeLayer = (uuid) ->
# FIXME: Edge-case: removing the last layer in a scene?
layerIndex = @orderedLayers.indexOf(@layers[uuid])
delete @layers[uuid]
[@orderedLayers, removedLayers] = splitFilter @orderedLayers, (layer) ->
layer.uuid != uuid
removedLayers.forEach (layerObject) =>
@emit "layer:removed", layerObject
# Remove all the objects on this layer as well...
@orderedObjects
.filter (object) -> object.layer == uuid
.forEach (object) => @removeObject(object.uuid)
if layerIndex == 0
newActiveIndex = 0
else
newActiveIndex = layerIndex - 1
@setActiveLayer(@orderedLayers[newActiveIndex]?.uuid)
setActiveLayer = (uuid) ->
@activeLayer = uuid
@orderedObjects.forEach (object) ->
object.editorFaded = (object.layer != uuid)
@emit "layer:switched", @layers[uuid]
getActiveLayer = ->
return @layers[@activeLayer]
# Object methods
setLayer = (uuid) ->
@emit "layer:switched", @scene.layers[uuid]
@layer = uuid
# Setup function
API = (stage) ->
stage.on "scene:added", (scene) ->
scene._ignoredProperties.push "orderedLayers"
scene.layers = {}
scene.orderedLayers = []
scene.createLayer = createLayer
scene.removeLayer = removeLayer
scene.setActiveLayer = setActiveLayer
scene.getActiveLayer = getActiveLayer
scene.on "object:added", (object) ->
object.layer = scene.activeLayer
object.setLayer = setLayer
scene.on "object:switched", (object) ->
if object.layer?
scene.setActiveLayer(object.layer)
process.nextTick ->
# Default first layer
scene.createLayer()
stage.on "scene:removed", (scene) ->
scene.orderedLayers.forEach (layer) ->
scene.removeLayer(layer.uuid)
return {}
API.meta =
name: "layers"
module.exports = API
# SPEC: list modifier for reordering the objects according to their layer...?

96
lib/object-panel.coffee

@ -0,0 +1,96 @@
$ = require "jquery"
domWait = require "../lib/dom-wait"
# Scene panel functions
createObjectItem = (uuid, name, sceneUuid) ->
list = @jqObject.find "ul.objectList"
$("<li></li>")
.appendTo list
.data "uuid", uuid
.data "scene-uuid", sceneUuid
.text name
removeObjectItem = (uuid) ->
@jqObject
.find "ul.objectList li"
.filter (i, li) -> $(li).data("uuid") == uuid
.remove()
switchObjectItem = (uuid) ->
@jqObject
.find "ul.objectList li"
.removeClass "selected"
.filter (i, li) -> $(li).data("uuid") == uuid
.addClass "selected"
# API methods
attachObjectPanel = (jqObject) ->
panel =
stage: this
jqObject: jqObject
createObjectItem: domWait.func(createObjectItem)
removeObjectItem: domWait.func(removeObjectItem)
switchObjectItem: domWait.func(switchObjectItem)
panel.stage.on "scene:added", (scene) ->
scene.on "object:added", (object) ->
domElement = panel.createObjectItem(object.uuid, object.name, scene.uuid)
# FIXME: For now assumes that we automatically activate the new object.
panel.switchObjectItem(object.uuid)
object.on "changed:editorFaded", (faded) ->
if faded
domElement.addClass "faded"
else
domElement.removeClass "faded"
scene.on "object:removed", (object) ->
panel.removeObjectItem(object.uuid)
scene.on "object:switched", (object) ->
panel.switchObjectItem(object.uuid)
panel.stage.on "scene:switched", (scene) ->
jqObject
.find "ul.objectList li"
.hide()
.filter (i, li) -> $(li).data("scene-uuid") == scene.uuid
.show()
if activeObject = scene.getActiveObject?()
panel.switchObjectItem activeObject.uuid
jqObject
.find ".action-addObject"
.on "click", (event) ->
activeScene = panel.stage.getActiveScene()
objectType = $(this).data("type")
objectName = "Object (#{objectType}) #{activeScene.getObjectNameIncrement()}"
newObject = activeScene.createObject(type: objectType, name: objectName)
jqObject
.find ".action-removeObject"
.on "click", (event) ->
activeScene = panel.stage.getActiveScene()
targetUuid = activeScene.getActiveObject().uuid
newObject = activeScene.removeObject(targetUuid)
jqObject
.find "ul.objectList"
.on "mousedown", "li", (event) ->
targetUuid = $(this).data "uuid"
activeScene = panel.stage.getActiveScene()
activeScene.setActiveObject targetUuid
# Public API
API = (stage) ->
return {
attach: attachObjectPanel.bind(stage)
}
API.meta =
name: "objectPanel"
module.exports = API

17
lib/object-type-image.coffee

@ -0,0 +1,17 @@
attributes =
source:
type: "string"
API = (stage) ->
stage.on "scene:added", (scene) ->
scene.on "object:created", (object) ->
if object.type == "image"
Object.keys(attributes).forEach (propName) ->
object.registerProperty propName, attributes[propName]
return {}
API.meta =
name: "objectTypeImage"
module.exports = API

32
lib/object-type-text.coffee

@ -0,0 +1,32 @@
attributes =
text:
type: "string"
html:
type: "html"
fontFamily:
type: "font"
fontSize:
type: "numeric"
bold:
type: "boolean"
italic:
type: "boolean"
fontColor:
type: "color"
API = (stage) ->
stage.on "scene:added", (scene) ->
scene.on "object:created", (object) ->
if object.type == "text"
Object.keys(attributes).forEach (propName) ->
object.registerProperty propName, attributes[propName]
process.nextTick ->
object.text = "Text goes here"
return {}
API.meta =
name: "objectTypeText"
module.exports = API

101
lib/property-panel.coffee

@ -0,0 +1,101 @@
$ = require "jquery"
domWait = require "../lib/dom-wait"
lockTimeout = 500
# Property panel functions
createPropertyItem = (name, type) ->
list = @jqObject.find "div.propertyList"
object = @stage.getActiveScene().getActiveObject()
# To prevent our changes from echoing
lastValue = null
domElement = $("<div class='property'><label></label></div>")
.data "property", name
.data "type", type
.appendTo list
domElement
.children "label"
.text name
setValue = (value) ->
if value == lastValue
return
lastValue = value
property = domElement.data("property")
object[property] = value
if type == "boolean"
# Checkboxes want to be special.
inputElement = $("<input type='checkbox'>")
inputElement
.prop "checked", object[name]
.on "change", (event) ->
setValue $(this).is ":checked"
object.on "changed:#{name}", (value) ->
if value == lastValue
return
lastValue = value
inputElement.prop "checked", value
else
inputElement = switch type
when "numeric"
$("<input type='numeric'>")
when "text"
$("<input type='text'>")
else
$("<input type='text'>")
inputElement
.val object[name]
.on "change input propertychange", (event) ->
setValue $(this).val()
object.on "changed:#{name}", (value) ->
if value == lastValue
return
lastValue = value
inputElement.val(value)
inputElement.appendTo domElement
removePropertyItems = (uuid) ->
@jqObject
.find "div.propertyList .property"
.remove()
# API methods
attachPropertyPanel = (jqObject) ->
panel =
stage: this
jqObject: jqObject
createPropertyItem: domWait.func(createPropertyItem)
removePropertyItems: domWait.func(removePropertyItems)
panel.stage.on "scene:added", (scene) ->
scene.on "object:switched", (object) ->
panel.removePropertyItems()
Object.keys(object.propMeta).forEach (property) ->
meta = object.propMeta[property]
console.log property, meta
panel.createPropertyItem property, meta.type
# Public API
API = (stage) ->
return {
attach: attachPropertyPanel.bind(stage)
}
API.meta =
name: "propertyPanel"
module.exports = API

72
lib/scene-panel.coffee

@ -0,0 +1,72 @@
$ = require "jquery"
domWait = require "../lib/dom-wait"
# Scene panel functions
createSceneItem = (uuid, name) ->
list = @jqObject.find "ul.sceneList"
$("<li></li>")
.appendTo list
.data "uuid", uuid
.text name
removeSceneItem = (uuid) ->
@jqObject
.find "ul.sceneList li"
.filter (i, li) -> $(li).data("uuid") == uuid
.remove()
switchSceneItem = (uuid) ->
@jqObject
.find "ul.sceneList li"
.removeClass "selected"
.filter (i, li) -> $(li).data("uuid") == uuid
.addClass "selected"
# API methods
attachScenePanel = (jqObject) ->
panel =
stage: this
jqObject: jqObject
createSceneItem: domWait.func(createSceneItem)
removeSceneItem: domWait.func(removeSceneItem)
switchSceneItem: domWait.func(switchSceneItem)
panel.stage.on "scene:added", (scene) ->
panel.createSceneItem(scene.uuid, scene.name)
# FIXME: For now assumes that we automatically activate the new scene.
panel.switchSceneItem(scene.uuid)
panel.stage.on "scene:removed", (scene) ->
panel.removeSceneItem(scene.uuid)
panel.stage.on "scene:switched", (scene) ->
panel.switchSceneItem(scene.uuid)
jqObject
.find ".action-addScene"
.on "click", (event) ->
activeScene = panel.stage.createScene()
jqObject
.find ".action-removeScene"
.on "click", (event) ->
targetUuid = panel.stage.getActiveScene().uuid
panel.stage.removeScene targetUuid
jqObject
.find "ul.sceneList"
.on "mousedown", "li", (event) ->
targetUuid = $(this).data "uuid"
panel.stage.setActiveScene targetUuid
# Public API
API = (stage) ->
return {
attach: attachScenePanel.bind(stage)
}
API.meta =
name: "scenePanel"
module.exports = API

11
lib/split-filter.coffee

@ -0,0 +1,11 @@
module.exports = (array, filterFunc) ->
matches = []
nonMatches = []
array.forEach (item, i) ->
if filterFunc(item, i)
matches.push item
else
nonMatches.push item
return [matches, nonMatches]

39
package.json

@ -0,0 +1,39 @@
{
"name": "authored",
"version": "1.0.0",
"description": "A modular framework for building authoring tools.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git://github.com/joepie91/node-authored"
},
"keywords": [
"authoring"
],
"author": "Sven Slootweg",
"license": "WTFPL",
"dependencies": {
"jquery": "^2.1.3",
"uuid": "^2.0.1"
},
"devDependencies": {
"coffee-loader": "^0.7.2",
"coffee-script": "^1.9.1",
"gulp": "~3.8.0",
"gulp-cached": "~0.0.3",
"gulp-jade": "^0.7.0",
"gulp-livereload": "~2.1.0",
"gulp-nodemon": "~1.0.4",
"gulp-plumber": "~0.6.3",
"gulp-remember": "~0.3.0",
"gulp-rename": "~1.2.0",
"gulp-sass": "^1.3.3",
"gulp-util": "~2.2.17",
"gulp-webpack": "^1.2.0",
"through2-spy": "^1.2.0",
"st": "^0.5.2"
}
}

23
test-inheritance.coffee

@ -0,0 +1,23 @@
class A
constructor: ->
someMethod: ->
console.log "a"
someOtherMethod: ->
console.log "2"
b = new A()
b.someMethod()
c = Object.create(b)
c.someMethod = ->
console.log "derp"
c.someMethod()
b.someMethod()
d = Object.create(c)
d.someMethod()
d.someOtherMethod()

89
test/style.css

@ -0,0 +1,89 @@
body {
font-family: sans-serif; }
body .main {
position: absolute;
left: 0px;
top: 0px;
bottom: 0px;
right: 260px; }
body .main .notice {
padding: 8px;
margin: 16px;
border: 1px solid red;
background-color: #ffdada; }
body .main .renderer {
background-color: lightgoldenrodyellow;
width: 800px;
height: 600px; }
body .main .renderer .rendererObject {
position: absolute;
cursor: default; }
body .main .renderer .rendererObject.activated {
outline: 1px dashed #720000;
cursor: move; }
body .sidebar {
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
width: 260px;
background-color: antiquewhite;
border-left: 1px solid gray; }
body .sidebar section h1 {
font-size: 17px;
font-weight: bold;
padding: 5px 7px;
margin: 0px;
background-color: #45674d;
color: white; }
body .sidebar section .action {
margin-left: 8px;
margin-top: 8px;
font-size: 13px;
border: 1px solid black;
background-color: white;
font-weight: bold;
padding: 3px 7px; }
body .sidebar section .action:hover {
background-color: #dddddd; }
body .sidebar section .toolbar {
border-bottom: 1px solid black;
background-color: white; }
body .sidebar section .toolbar .action {
border: 0px;
margin: 0px;
border-right: 1px solid black; }
body .sidebar section .layers ul, body .sidebar section .scenes ul, body .sidebar section .objects ul {
padding-left: 0px;
margin: 0px 0px 8px 0px;
border-bottom: 1px solid black;
background-color: azure; }
body .sidebar section .layers ul li, body .sidebar section .scenes ul li, body .sidebar section .objects ul li {
list-style-type: none;
padding: 3px 5px;
font-size: 14px; }
body .sidebar section .layers ul li.selected, body .sidebar section .scenes ul li.selected, body .sidebar section .objects ul li.selected {
background-color: #caf5f5;
color: red;
font-weight: bold; }
body .sidebar section .layers ul li.faded, body .sidebar section .scenes ul li.faded, body .sidebar section .objects ul li.faded {
color: gray; }
body .sidebar section .layers ul, body .sidebar section .scenes ul, body .sidebar section .objects ul, body .sidebar section .properties ul {
height: 100px;
overflow: auto; }
body .sidebar section .properties .property {
margin-bottom: 2px; }
body .sidebar section .properties .property:before, body .sidebar section .properties .property:after {
content: " ";
display: table; }
body .sidebar section .properties .property:after {
clear: both; }
body .sidebar section .properties .property label {
display: block;
float: left;
width: 90px;
padding: 3px 5px;
font-size: 14px;
background-color: #e2d2bd; }
body .sidebar section .properties .property input {
max-width: 150px; }

168
test/style.scss

@ -0,0 +1,168 @@
body
{
font-family: sans-serif;
.main
{
position: absolute;
left: 0px;
top: 0px;
bottom: 0px;
right: 260px;
.notice
{
padding: 8px;
margin: 16px;
border: 1px solid red;
background-color: #ffdada;
}
.renderer
{
background-color: lightgoldenrodyellow;
width: 800px;
height: 600px;
.rendererObject
{
position: absolute;
cursor: default;
&.activated
{
outline: 1px dashed #720000;
cursor: move;
}
}
}
}
.sidebar
{
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
width: 260px;
background-color: antiquewhite;
border-left: 1px solid gray;
section
{
h1
{
font-size: 17px;
font-weight: bold;
padding: 5px 7px;
margin: 0px;
background-color: #45674d;
color: white;
}
.action
{
margin-left: 8px;
margin-top: 8px;
font-size: 13px;
border: 1px solid black;
background-color: white;
font-weight: bold;
padding: 3px 7px;
&:hover
{
background-color: #dddddd;
}
}
.toolbar
{
border-bottom: 1px solid black;
background-color: white;
.action
{
border: 0px;
margin: 0px;
border-right: 1px solid black;
}
}
.layers, .scenes, .objects
{
ul
{
padding-left: 0px;
margin: 0px 0px 8px 0px;
border-bottom: 1px solid black;
background-color: azure;
li
{
list-style-type: none;
padding: 3px 5px;
font-size: 14px;
&.selected
{
background-color: #caf5f5;
color: red;
font-weight: bold;
}
&.faded
{
color: gray;
}
}
}
}
.layers, .scenes, .objects, .properties
{
ul
{
height: 100px;
overflow: auto;
}
}
.properties
{
.property
{
margin-bottom: 2px;
&:before, &:after
{
content: " ";
display: table;
}
&:after
{
clear: both;
}
label
{
display: block;
float: left;
width: 90px;
padding: 3px 5px;
font-size: 14px;
background-color: #e2d2bd;
}
input
{
max-width: 150px;
}
}
}
}
}
}

10955
test/test-bundle.js
File diff suppressed because it is too large
View File

1
test/test.html

@ -0,0 +1 @@
<html><head><script src="test-bundle.js"></script><link rel="stylesheet" href="style.css"/></head><body><div class="main"><div class="notice"><strong>This demo is <em>not</em> representative of the final functionality or design!</strong> This is a technical demo, purely for the purpose of displaying some of the functionality that could be implemented. The authoring tool is intentionally a small, modular framework that allows for implementing pretty much <em>any</em> kind of functionality.</div><div class="renderer"></div></div><div class="sidebar"><section id="scenes"><h1>Scenes</h1><div class="scenes"><div class="toolbar"><button class="action action-addScene">Add scene</button><button class="action action-removeScene">Remove scene</button></div><ul class="sceneList"></ul></div></section><section id="layers"><h1>Layers</h1><div class="layers"><div class="toolbar"><button class="action action-addLayer">Add layer</button><button class="action action-removeLayer">Remove layer</button></div><ul class="layerList"></ul></div></section><section id="objects"><h1>Objects</h1><div class="objects"><div class="toolbar"><!-- button.action.action-addObject Add object--><button data-type="text" class="action action-addObject">+ Text</button><button data-type="image" class="action action-addObject">+ Image</button><button data-type="video" class="action action-addObject">+ Video</button><button class="action action-removeObject">Remove</button></div><ul class="objectList"></ul></div></section><section id="properties"><h1>Properties</h1><div class="properties"><div class="propertyList"></div></div></section></div></body></html>

42
test/test.jade

@ -0,0 +1,42 @@
html
head
script(src="test-bundle.js")
link(rel="stylesheet", href="style.css")
body
.main
.notice
strong This demo is <em>not</em> representative of the final functionality or design!
| This is a technical demo, purely for the purpose of displaying some of the functionality that could be implemented. The authoring tool is intentionally a small, modular framework that allows for implementing pretty much <em>any</em> kind of functionality.
.renderer
.sidebar
section#scenes
h1 Scenes
div.scenes
.toolbar
button.action.action-addScene Add scene
button.action.action-removeScene Remove scene
ul.sceneList
section#layers
h1 Layers
div.layers
.toolbar
button.action.action-addLayer Add layer
button.action.action-removeLayer Remove layer
ul.layerList
section#objects
h1 Objects
div.objects
.toolbar
// button.action.action-addObject Add object
button.action.action-addObject(data-type="text") + Text
button.action.action-addObject(data-type="image") + Image
button.action.action-addObject(data-type="video") + Video
button.action.action-removeObject Remove
ul.objectList
section#properties
h1 Properties
div.properties
div.propertyList

20
test/test.js

@ -0,0 +1,20 @@
var authored = require("../lib/");
var $ = require("jquery");
var stage = new authored();
stage.use(require("../lib/layers"));
stage.use(require("../lib/layer-panel"));
stage.use(require("../lib/scene-panel"));
stage.use(require("../lib/object-panel"));
stage.use(require("../lib/property-panel"));
stage.use(require("../lib/object-type-text"));
stage.use(require("../lib/object-type-image"));
stage.use(require("../lib/html-renderer"));
$(function(){
stage.plugins.layerPanel.attach($(".layers"));
stage.plugins.scenePanel.attach($(".scenes"));
stage.plugins.objectPanel.attach($(".objects"));
stage.plugins.propertyPanel.attach($(".properties"));
stage.plugins.htmlRenderer.attach($(".renderer"));
});
Loading…
Cancel
Save