improve design patterns example
This commit is contained in:
parent
9b79a0e585
commit
a299d2f6b1
1 changed files with 516 additions and 301 deletions
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The following examples explain the common design pattern for OxJS UI widgets: an
|
The following examples explain the common design pattern for OxJS UI widgets: an
|
||||||
inheritance model that is neither classical nor prototypal, but "parasitical" (a
|
inheritance model that is neither classical nor prototypal, but "parasitical" (a
|
||||||
|
@ -13,346 +12,562 @@ be accessed from outside, but since `self` itself is an argument of the
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Load the UI module.
|
Create our own namespace. Not required, but if you wanted to create a
|
||||||
|
module named `My`, this is how you would do it.
|
||||||
*/
|
*/
|
||||||
Ox.load(['Image', 'UI'], function() {
|
Ox.My = {};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<hr>
|
||||||
|
First, lets build the most basic Box widget. A widget is a "constructor"
|
||||||
|
function that takes two (optional) arguments, `options` and `self`, and returns
|
||||||
|
a widget object. It's not a constructor in JavaScript terms though: It doesn't
|
||||||
|
have to be called with `new`, and doesn't return an `instanceof` anything. It
|
||||||
|
just enhances another widget object and returns it.
|
||||||
|
*/
|
||||||
|
Ox.My.Box = function(options, self) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Create our own namespace.
|
This is how every widget "constructor" begins. `self` is the widget's shared
|
||||||
|
private object.
|
||||||
*/
|
*/
|
||||||
Ox.My = {};
|
self = self || {};
|
||||||
|
/*
|
||||||
|
`that` is the widget itself, its public object, or, in JavaScript terms, its
|
||||||
|
`this`. Every widget "inherits" from another widget by simple assignment.
|
||||||
|
All public properties of the "super" widget, i.e. all properties of its
|
||||||
|
`that`, will be present on our own `that`. In this case, we use Ox.Element,
|
||||||
|
the "root" widget at the end of the inheritance chain, and pass an empty
|
||||||
|
options object. But we always pass our own `self`, which means that any
|
||||||
|
property that Ox.Element (or any other widget in the inheritance chain) adds
|
||||||
|
to `self` will be present on our own `self`.
|
||||||
|
*/
|
||||||
|
var that = Ox.Element({}, self)
|
||||||
|
/*
|
||||||
|
Then we call the public `defaults`, `options` and `update` methods of
|
||||||
|
Ox.Element. `defaults` assigns the defaults object to `self.defaults`
|
||||||
|
and copies it to `self.options`, `options` extends `self.options` with
|
||||||
|
the options object, and `update` adds one or more callbacks that are
|
||||||
|
invoked whenever, by way of calling the `options` method, a property of
|
||||||
|
`self.options` is modified or added.
|
||||||
|
*/
|
||||||
|
.defaults({
|
||||||
|
color: [128, 128, 128],
|
||||||
|
size: [128, 128]
|
||||||
|
})
|
||||||
|
.options(options || {})
|
||||||
|
.update({
|
||||||
|
color: setColor,
|
||||||
|
size: setSize
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
`addClass` is a jQuery method. In fact, Ox.Element (and any widget
|
||||||
|
derived from it) provides, on its prototype, all methods of a jQuery
|
||||||
|
`$('<div>')`. Chaining works too: If you have `var $d = $('<div>'), $e =
|
||||||
|
Ox.Element();`, then `$d.appendTo($e)` returns `$d`, and `$e.append($d)`
|
||||||
|
returns `$e`. If you type Ox.Element() in the console, you will get
|
||||||
|
something like `[<div class="OxElement"></div>]`. Any widget's `0`
|
||||||
|
property is an actual DOM element, and in case you ever need the
|
||||||
|
jQuery-wrapped element — that's the widget's `$element` property.
|
||||||
|
|
||||||
|
The purpose of the `OxMyBox` class is just to allow us to add CSS
|
||||||
|
declarations in an external style sheet. In this case, `.css({float:
|
||||||
|
'left'})` would do the same thing.
|
||||||
|
*/
|
||||||
|
.addClass('OxMyBox');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
First, lets build the most basic Box widget. A widget is a "constructor"
|
The second part of the "constructor" function can be thought of as the
|
||||||
function that takes two (optional) arguments, `options` and `self`, and
|
"initializer", and contains everything needed to set up the "instance". In
|
||||||
returns a widget object. It's not a constructor in JavaScript terms though:
|
this case, we just define a minimum and maximum size and then set the
|
||||||
It doesn't have to be called with `new`, and doesn't return an `instanceof`
|
widget's color and size.
|
||||||
anything. It just enhances another widget object and returns it.
|
|
||||||
|
We could have used `var minSize` and `var maxSize` here, but by using `self`
|
||||||
|
for private variables that we want to be accessible across all the widget's
|
||||||
|
methods, we can be sure that inside such methods, any local `var` is
|
||||||
|
actually local to the method.
|
||||||
*/
|
*/
|
||||||
Ox.My.Box = function(options, self) {
|
self.minSize = 1;
|
||||||
|
self.maxSize = 256;
|
||||||
|
|
||||||
|
setColor();
|
||||||
|
setSize();
|
||||||
|
|
||||||
|
/*
|
||||||
|
Third, we declare the widget's private methods. These are just function
|
||||||
|
declarations, hoisted to the top of the "constructor".
|
||||||
|
*/
|
||||||
|
function setColor() {
|
||||||
|
that.css({
|
||||||
|
backgroundColor: 'rgb(' + self.options.color.join(', ') + ')',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSize() {
|
||||||
/*
|
/*
|
||||||
This is how every widget "constructor" begins. `self` is the widget's
|
Before setting the size, we make sure the value is between `minSize` and
|
||||||
shared private object.
|
`maxSize`.
|
||||||
*/
|
*/
|
||||||
self = self || {};
|
self.options.size = self.options.size.map(function(value) {
|
||||||
|
return Ox.limit(value, self.minSize, self.maxSize);
|
||||||
|
});
|
||||||
|
that.css({
|
||||||
|
width: self.options.size[0] + 'px',
|
||||||
|
height: self.options.size[1] + 'px'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Next, we define the widgets public methods, as properties of `that`. (Note
|
||||||
|
that unlike private methods, they are not hoisted.)
|
||||||
|
*/
|
||||||
|
that.displayText = function(text) {
|
||||||
/*
|
/*
|
||||||
`that` is the widget itself, its public object, or, in JavaScript terms,
|
As there isn't much to do yet, this method just displays some text.
|
||||||
its `this`. Every widget "inherits" from another widget by simple
|
Here, `.addClass('OxMyText')` is equivalent to `.css({padding: '4px'})`.
|
||||||
assignment. All public properties of the "super" widget, i.e. all
|
|
||||||
properties of its `that`, will be present on our own `that`. In this
|
|
||||||
case, we use Ox.Element, the "root" widget at the end of the inheritance
|
|
||||||
chain, and pass an empty options object. But we always pass our own
|
|
||||||
`self`, which means that any property that Ox.Element (or any other
|
|
||||||
widget in the inheritance chain) adds to `self` will be present on our
|
|
||||||
own `self`. Then we call the public `defaults`, `options` and `update`
|
|
||||||
methods of Ox.Element. `defaults` assigns the defaults object to
|
|
||||||
`self.defaults` and copies it to `self.options`, `options` extends
|
|
||||||
`self.options` with the options object, and `update` adds one or more
|
|
||||||
callbacks that are invoked whenever, by way of calling the `options`
|
|
||||||
method, a property of `self.options` is modified or added.
|
|
||||||
*/
|
*/
|
||||||
var that = Ox.Element({}, self)
|
that.empty();
|
||||||
.defaults({
|
text && that.append($('<div>').addClass('OxMyText').html(text));
|
||||||
color: [128, 128, 128],
|
|
||||||
size: [128, 128]
|
|
||||||
})
|
|
||||||
.options(options || {})
|
|
||||||
.update({
|
|
||||||
color: setColor,
|
|
||||||
size: setSize
|
|
||||||
})
|
|
||||||
.addClass('OxMyBox');
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The second part of the "constructor" function can be thought of as the
|
Public methods should return `that`, for chaining.
|
||||||
"initializer", and contains everything needed to set up the "instance".
|
|
||||||
In this case, we just define a minimum and maximum size and then set the
|
|
||||||
widget's color and size. We could have used `var minSize` and `var
|
|
||||||
maxSize` here, but by using `self` for private variables that we want to
|
|
||||||
be accessible across all the widget's methods, we can be sure that
|
|
||||||
inside such methods, any local `var` is actually local to the method.
|
|
||||||
*/
|
|
||||||
self.minSize = 1;
|
|
||||||
self.maxSize = 256;
|
|
||||||
|
|
||||||
setColor();
|
|
||||||
setSize();
|
|
||||||
|
|
||||||
/*
|
|
||||||
Third, we declare the widget's private methods. These are just function
|
|
||||||
declarations, hoisted to the top of the "constructor".
|
|
||||||
*/
|
|
||||||
function setColor() {
|
|
||||||
/*
|
|
||||||
To interact with the DOM, Ox.Element (and any widget derived from
|
|
||||||
it) wraps jQuery. If you type Ox.Element() in the console, you
|
|
||||||
will get something like `[<div class="OxElement"></div>]`, and the
|
|
||||||
widget's prototype has all the methods of a `$('<div>')`, with
|
|
||||||
proper chaining. If you have `var $d = $('<div>'), $e =
|
|
||||||
Ox.Element();`, then `$d.appendTo($e)` returns `$d`, and
|
|
||||||
`$e.append($d)` returns `$e`.
|
|
||||||
*/
|
|
||||||
that.css({
|
|
||||||
backgroundColor: 'rgb(' + self.options.color.join(', ') + ')',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSize() {
|
|
||||||
/*
|
|
||||||
Before setting the size, we make sure the value is between `minSize`
|
|
||||||
and `maxSize`.
|
|
||||||
*/
|
|
||||||
self.options.size = self.options.size.map(function(value) {
|
|
||||||
return Ox.limit(value, self.minSize, self.maxSize);
|
|
||||||
});
|
|
||||||
that.css({
|
|
||||||
width: self.options.size[0] + 'px',
|
|
||||||
height: self.options.size[1] + 'px'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Next, we define the widgets public methods, as properties of `that`.
|
|
||||||
(Note that unlike private methods, they are not hoisted.)
|
|
||||||
*/
|
|
||||||
that.displayText = function(text) {
|
|
||||||
/*
|
|
||||||
As there isn't much to do yet, this method just displays the
|
|
||||||
widget's options.
|
|
||||||
*/
|
|
||||||
that.empty();
|
|
||||||
text && that.append($('<div>').addClass('OxMyText').html(text));
|
|
||||||
/*
|
|
||||||
Public methods should return `that`, for chaining.
|
|
||||||
*/
|
|
||||||
return that;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
And finally, at the very end of the "constructor", we return `that`. And
|
|
||||||
that's it.
|
|
||||||
*/
|
*/
|
||||||
return that;
|
return that;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ox.My.InvertibleBox = function(options, self) {
|
/*
|
||||||
|
And finally, at the very end of the "constructor", we return `that`. And
|
||||||
|
that's it.
|
||||||
|
*/
|
||||||
|
return that;
|
||||||
|
|
||||||
self = self || {};
|
};
|
||||||
var that = Ox.My.Box({}, self);
|
|
||||||
that.defaults(Ox.extend(that.defaults(), {
|
|
||||||
inverted: false
|
|
||||||
}))
|
|
||||||
.options(options || {})
|
|
||||||
.update({
|
|
||||||
color: setColor,
|
|
||||||
inverted: setColor
|
|
||||||
})
|
|
||||||
.addClass('OxMyInvertibleBox')
|
|
||||||
.bindEvent({
|
|
||||||
doubleclick: function() {
|
|
||||||
that.invert();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.options.inverted && setColor();
|
/*
|
||||||
|
<hr>
|
||||||
|
Now we can "subclass" our Box. Let's build one that can have its color inverted.
|
||||||
|
*/
|
||||||
|
Ox.My.InvertibleBox = function(options, self) {
|
||||||
|
|
||||||
function getInvertedColor() {
|
self = self || {};
|
||||||
return self.options.color.map(function(value) {
|
/*
|
||||||
|
We no longer inherit from Ox.Element, but from `Ox.My.Box`.
|
||||||
|
We could have written
|
||||||
|
<pre>
|
||||||
|
var that = Ox.My.Box({}, self)
|
||||||
|
.defaults({
|
||||||
|
color: [128, 128, 128],
|
||||||
|
inverted: false,
|
||||||
|
size: [128, 128]
|
||||||
|
})
|
||||||
|
.options(options || ())
|
||||||
|
.update({
|
||||||
|
...
|
||||||
|
})
|
||||||
|
</pre>
|
||||||
|
— but why repeat the defaults of `Ox.My.Box` if we can simply extend
|
||||||
|
them. (Just like `options()` returns all options of a widget, `defaults()`
|
||||||
|
returns all its defaults.)
|
||||||
|
*/
|
||||||
|
var that = Ox.My.Box({}, self);
|
||||||
|
that.defaults(Ox.extend(that.defaults(), {
|
||||||
|
inverted: false
|
||||||
|
}))
|
||||||
|
.options(options || {})
|
||||||
|
/*
|
||||||
|
Again, we add handlers that run when the widget's options are updated.
|
||||||
|
The original handlers of `Ox.My.Box` will run next, so we just add the
|
||||||
|
ones we need. We leave out `size`, so when the `size` option changes,
|
||||||
|
we'll get the original behavior.
|
||||||
|
*/
|
||||||
|
.update({
|
||||||
|
color: setColor,
|
||||||
|
inverted: setColor
|
||||||
|
})
|
||||||
|
/*
|
||||||
|
The same as `.css({cursor: 'pointer'})`.
|
||||||
|
*/
|
||||||
|
.addClass('OxMyInvertibleBox')
|
||||||
|
/*
|
||||||
|
Ox.Element and its descendants provide a number of public methods
|
||||||
|
(`bindEvent`, `bindEventOnce`, `triggerEvent` and `unbindEvent`) that
|
||||||
|
allow widgets to communicate via custom events. Here, we add a handler
|
||||||
|
for Ox.Element's `doubleclick` event. If we just wanted to handle a
|
||||||
|
`click` event, we could also use jQuery here:
|
||||||
|
<pre>
|
||||||
|
.on({
|
||||||
|
click: function() {
|
||||||
|
that.invert();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</pre>
|
||||||
|
*/
|
||||||
|
.bindEvent({
|
||||||
|
doubleclick: function() {
|
||||||
|
that.invert();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
The idea is that our widget's inverted state is separate from its color. If
|
||||||
|
the inverted option is set, then the color option stays the same, but has
|
||||||
|
the inverse effect. This means that when initializing the widget, we have
|
||||||
|
to call our custom `setColor` method if `self.options.inverted` is `true`.
|
||||||
|
*/
|
||||||
|
self.options.inverted && setColor();
|
||||||
|
|
||||||
|
/*
|
||||||
|
When `setColor` is invoked as an update handler, returning `false` signals
|
||||||
|
that no other handler should run. Otherwise, the original handler of
|
||||||
|
`Ox.My.Box` would run next, and revert any inversion we might have done
|
||||||
|
here.
|
||||||
|
*/
|
||||||
|
function setColor() {
|
||||||
|
that.css({backgroundColor: 'rgb(' + (
|
||||||
|
self.options.inverted ? self.options.color.map(function(value) {
|
||||||
return 255 - value;
|
return 255 - value;
|
||||||
});
|
}) : self.options.color
|
||||||
}
|
).join(', ') + ')'});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
function setColor() {
|
/*
|
||||||
that.css({backgroundColor: 'rgb(' + (
|
The public `invert` method is added as a convenience for the users of our
|
||||||
self.options.inverted ? getInvertedColor() : self.options.color
|
widget, so that when they want to toggle its inverted state, they don't have
|
||||||
).join(', ') + ')'});
|
to write
|
||||||
|
<pre>
|
||||||
|
$widget.options({
|
||||||
|
inverted: !$widget.options('inverted')
|
||||||
|
});
|
||||||
|
</pre>
|
||||||
|
all the time.
|
||||||
|
|
||||||
|
Also, we trigger an `invert` event, that anyone can bind to via
|
||||||
|
<pre>
|
||||||
|
$widget.bindEvent({
|
||||||
|
invert: function() { ... }
|
||||||
|
});
|
||||||
|
</pre>
|
||||||
|
*/
|
||||||
|
that.invert = function() {
|
||||||
|
that.options({inverted: !self.options.inverted});
|
||||||
|
that.triggerEvent('invert');
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
And again, we return `that`.
|
||||||
|
*/
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<hr>
|
||||||
|
Now it's time for something more funky: A MetaBox — that is, a box of
|
||||||
|
boxes.
|
||||||
|
*/
|
||||||
|
Ox.My.MetaBox = function(options, self) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
This time, we inherit from `Ox.My.InvertibleBox`. The one thing that's
|
||||||
|
different though is the `color` option: It is no longer a single value, but
|
||||||
|
an array of array of values. That's how the boxes inside out meta-box are
|
||||||
|
specified. The following would create a grid of boxes with two rows and
|
||||||
|
three columns:
|
||||||
|
<pre>
|
||||||
|
Ox.My.MetaBox({
|
||||||
|
color: [
|
||||||
|
[[255, 0, 0], [255, 255, 0], [0, 255, 0]],
|
||||||
|
[[0, 255, 255], [0, 255, 0], [255, 0, 255]]
|
||||||
|
]
|
||||||
|
});
|
||||||
|
</pre>
|
||||||
|
*/
|
||||||
|
self = self || {};
|
||||||
|
var that = Ox.My.InvertibleBox({}, self)
|
||||||
|
.options(options || {})
|
||||||
|
.update({color: setColor});
|
||||||
|
|
||||||
|
/*
|
||||||
|
But we keep the default color of `Ox.My.InvertibleBox` (`[128, 128, 128]`)
|
||||||
|
as our own default color, and only here check if the color option is a
|
||||||
|
single value. In that case, we convert it to an array of one row and one
|
||||||
|
column. This way, whenever someone accidentally passes a single color value,
|
||||||
|
our MetaBox can handle it.
|
||||||
|
*/
|
||||||
|
if (Ox.isNumber(self.options.color[0])) {
|
||||||
|
self.options.color = [[self.options.color]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
`self.sizes` holds the width of each column and the height of each row.
|
||||||
|
`self.options.color.length` is the number of rows,
|
||||||
|
`self.options.color[0].length` the number of columns, and Ox.splitInt(a, b)
|
||||||
|
"splits" an integer `a` into an array of `b` integers that sum up to `a`.
|
||||||
|
(We don't want fractional pixel sizes.)
|
||||||
|
*/
|
||||||
|
self.sizes = [
|
||||||
|
Ox.splitInt(self.options.size[0], self.options.color[0].length),
|
||||||
|
Ox.splitInt(self.options.size[1], self.options.color.length)
|
||||||
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
|
`self.$boxes` are the actual boxes. We use `Ox.My.InvertibleBox`, but
|
||||||
|
remove their `doubleclick` handlers, since our meta-box already has one.
|
||||||
|
(`unbindEvent(event)` removes all handlers, `unbindEvent(event, handler)`
|
||||||
|
removes a specific one.) Then we simply append each box to the meta-box.
|
||||||
|
*/
|
||||||
|
self.$boxes = self.options.color.map(function(array, y) {
|
||||||
|
return array.map(function(color, x) {
|
||||||
|
return Ox.My.InvertibleBox({
|
||||||
|
color: color,
|
||||||
|
size: [self.sizes[0][x], self.sizes[1][y]]
|
||||||
|
})
|
||||||
|
.unbindEvent('doubleclick')
|
||||||
|
.appendTo(that);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
To set the color of a meta-box means to set the color of each box.
|
||||||
|
*/
|
||||||
|
function setColor() {
|
||||||
|
self.$boxes.forEach(function(array, y) {
|
||||||
|
array.forEach(function($box, x) {
|
||||||
|
$box.options({color: self.options.color[y][x]});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is the rare case of a shared private method. Its purpose will become
|
||||||
|
apparent a bit later. Otherwise, we could just have made a private function,
|
||||||
|
or an anonymous function in the loop below.
|
||||||
|
*/
|
||||||
|
self.invertBox = function($box) {
|
||||||
|
$box.invert();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Here, we override the public `invert` method of `Ox.My.InvertibleBox`. When
|
||||||
|
inverting an `Ox.My.MetaBox`, we have to invert each of its boxes. (If we
|
||||||
|
wanted to keep the original method around, we could store it as
|
||||||
|
`that.superInvert` before.)
|
||||||
|
*/
|
||||||
|
that.invert = function() {
|
||||||
|
self.$boxes.forEach(function(array) {
|
||||||
|
array.forEach(self.invertBox);
|
||||||
|
});
|
||||||
|
that.triggerEvent('invert');
|
||||||
|
return that;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
And that's all it takes to make a meta-box.
|
||||||
|
*/
|
||||||
|
return that;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
<hr>
|
||||||
|
The next widget is a peculiar type of meta-box. A PixelBox has only one color,
|
||||||
|
but this color will be split into a red box, a green box and a blue box.
|
||||||
|
*/
|
||||||
|
Ox.My.PixelBox = function(options, self) {
|
||||||
|
|
||||||
|
self = self || {};
|
||||||
|
/*
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
self.options = Ox.extend(Ox.My.Box().defaults(), options || {});
|
||||||
|
var that = Ox.My.MetaBox(Ox.extend(self.options, {
|
||||||
|
color: getColor()
|
||||||
|
}), self)
|
||||||
|
.update({
|
||||||
|
color: setColor
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is how the color gets split up into a red box, a green box and a blue
|
||||||
|
box.
|
||||||
|
*/
|
||||||
|
function getColor() {
|
||||||
|
return [[
|
||||||
|
[self.options.color[0], 0, 0],
|
||||||
|
[0, self.options.color[1], 0],
|
||||||
|
[0, 0, self.options.color[2]]
|
||||||
|
]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
...
|
||||||
|
*/
|
||||||
|
function setColor() {
|
||||||
|
if (Ox.isNumber(self.options.color[0])) {
|
||||||
|
that.options({color: getColor()});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
that.invert = function() {
|
/*
|
||||||
that.options({inverted: !self.options.inverted});
|
Inverting a PixelBox is different from inverting a MetaBox, since we only
|
||||||
that.triggerEvent('invert');
|
want to invert one color channel per box. This is where the shared private
|
||||||
return that;
|
`invertBox` method of `Ox.My.MetaBox` comes into play. Since we share the
|
||||||
};
|
same `self`, we can simply override it. (Alternatively, we could have added
|
||||||
|
an `invertBox` option to `Ox.My.MetaBox`, but overriding a shared private
|
||||||
return that;
|
method is much more elegant than cluttering the public API of
|
||||||
|
`Ox.My.MetaBox` with such an option.)
|
||||||
|
*/
|
||||||
|
self.invertBox = function($box, x) {
|
||||||
|
$box.options({
|
||||||
|
color: $box.options('color').map(function(value, i) {
|
||||||
|
return i == x ? 255 - value : value
|
||||||
|
})
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Ox.My.MetaBox = function(options, self) {
|
return that;
|
||||||
|
|
||||||
self = self || {};
|
};
|
||||||
var that = Ox.My.Box({}, self);
|
|
||||||
that.defaults(Ox.extend(that.defaults(), {
|
|
||||||
color: [[[128, 128, 128]]]
|
|
||||||
}))
|
|
||||||
.options(options || {})
|
|
||||||
.update({color: setColor})
|
|
||||||
.bindEvent({
|
|
||||||
doubleclick: function() {
|
|
||||||
that.invert();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.sizes = [
|
/*
|
||||||
Ox.splitInt(self.options.size[0], self.options.color[0].length),
|
<hr>
|
||||||
Ox.splitInt(self.options.size[1], self.options.color.length)
|
And finally — a meta-meta-box! An ImageBox takes an image and, for each
|
||||||
];
|
pixel, displays a PixelBox.
|
||||||
|
*/
|
||||||
|
Ox.My.ImageBox = function(options, self) {
|
||||||
|
|
||||||
self.$boxes = self.options.color.map(function(array, y) {
|
self = self || {};
|
||||||
return array.map(function(color, x) {
|
/*
|
||||||
return Ox.My.InvertibleBox({
|
Loading the image is asynchronous, but we want to display a box immediately.
|
||||||
color: color,
|
So we just subclass `Ox.My.Box`. Also, this seems to be a good use case for
|
||||||
|
its `displayText` method.
|
||||||
|
*/
|
||||||
|
var that = Ox.My.Box({}, self).displayText('Loading...');
|
||||||
|
that.defaults(Ox.extend(that.defaults(), {
|
||||||
|
image: null
|
||||||
|
}))
|
||||||
|
.options(options || {});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ox.Image takes a URI and passes an image object to its callback function.
|
||||||
|
*/
|
||||||
|
self.options.image && Ox.Image(self.options.image, function(image) {
|
||||||
|
var size = image.getSize();
|
||||||
|
size = [size.width, size.height];
|
||||||
|
/*
|
||||||
|
Again, we have to compute the width of each column and the height of
|
||||||
|
each row.
|
||||||
|
*/
|
||||||
|
self.sizes = size.map(function(value, index) {
|
||||||
|
return Ox.splitInt(self.options.size[index], value);
|
||||||
|
});
|
||||||
|
/*
|
||||||
|
Remove the 'Loading...' message.
|
||||||
|
*/
|
||||||
|
that.displayText();
|
||||||
|
/*
|
||||||
|
For each pixel ...
|
||||||
|
*/
|
||||||
|
self.$boxes = Ox.range(size[1]).map(function(y) {
|
||||||
|
return Ox.range(size[0]).map(function(x) {
|
||||||
|
/*
|
||||||
|
... create a PixelBox ...
|
||||||
|
*/
|
||||||
|
return Ox.My.PixelBox({
|
||||||
|
/*
|
||||||
|
(`image.pixel` returns RGBA, so discard alpha)
|
||||||
|
*/
|
||||||
|
color: image.pixel(x, y).slice(0, 3),
|
||||||
size: [self.sizes[0][x], self.sizes[1][y]]
|
size: [self.sizes[0][x], self.sizes[1][y]]
|
||||||
})
|
})
|
||||||
|
/*
|
||||||
|
... remove its `doubleclick` handler ...
|
||||||
|
*/
|
||||||
.unbindEvent('doubleclick')
|
.unbindEvent('doubleclick')
|
||||||
|
/*
|
||||||
|
... and append it to the ImageBox.
|
||||||
|
*/
|
||||||
.appendTo(that);
|
.appendTo(that);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
function setColor() {
|
|
||||||
self.$boxes.forEach(function(array, y) {
|
|
||||||
array.forEach(function($box, x) {
|
|
||||||
$box.options({color: self.options.color[y][x]});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.invertBox = function($box) {
|
|
||||||
$box.invert();
|
|
||||||
};
|
|
||||||
|
|
||||||
that.invert = function() {
|
|
||||||
self.$boxes.forEach(function(array) {
|
|
||||||
array.forEach(self.invertBox);
|
|
||||||
});
|
|
||||||
that.triggerEvent('invert');
|
|
||||||
return that;
|
|
||||||
};
|
|
||||||
|
|
||||||
return that;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
Ox.My.PixelBox = function(options, self) {
|
|
||||||
|
|
||||||
self = self || {};
|
|
||||||
self.options = Ox.extend(Ox.My.Box().defaults(), options || {});
|
|
||||||
var that = Ox.My.MetaBox(Ox.extend(self.options, {
|
|
||||||
color: getColor()
|
|
||||||
}), self)
|
|
||||||
.update({
|
|
||||||
color: function() {
|
|
||||||
setColor();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.invertBox = function($box, x) {
|
|
||||||
$box.options({
|
|
||||||
color: $box.options('color').map(function(value, i) {
|
|
||||||
return i == x ? 255 - value : value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function getColor() {
|
|
||||||
return [[
|
|
||||||
[self.options.color[0], 0, 0],
|
|
||||||
[0, self.options.color[1], 0],
|
|
||||||
[0, 0, self.options.color[2]]
|
|
||||||
]];
|
|
||||||
}
|
|
||||||
|
|
||||||
function setColor() {
|
|
||||||
self.$pixel.options({color: getColor()});
|
|
||||||
}
|
|
||||||
|
|
||||||
return that;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
Ox.My.ImageBox = function(options, self) {
|
|
||||||
|
|
||||||
self = self || {};
|
|
||||||
var that = Ox.My.Box({}, self).displayText('Loading...')
|
|
||||||
that.defaults({
|
|
||||||
image: null
|
|
||||||
})
|
|
||||||
.options(options || {});
|
|
||||||
|
|
||||||
Ox.Image(self.options.image, function(image) {
|
|
||||||
var size = image.getSize();
|
|
||||||
size = [size.width, size.height];
|
|
||||||
self.sizes = size.map(function(value, index) {
|
|
||||||
return Ox.splitInt(self.options.size[index], value);
|
|
||||||
});
|
|
||||||
that.displayText();
|
|
||||||
self.$boxes = Ox.range(size[1]).map(function(y) {
|
|
||||||
return Ox.range(size[0]).map(function(x) {
|
|
||||||
return Ox.My.PixelBox({
|
|
||||||
color: image.pixel(x, y).slice(0, 3),
|
|
||||||
size: [self.sizes[0][x], self.sizes[1][y]]
|
|
||||||
})
|
|
||||||
.unbindEvent('doubleclick')
|
|
||||||
.appendTo(that);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
that.invert = Ox.My.MetaBox(self.options, self).invert;
|
|
||||||
|
|
||||||
return that;
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This is left as an exercise for the reader ;)
|
We've inherited from `Ox.My.Box`, so we don't have an `invert` method yet.
|
||||||
|
This is how we can borrow the one from `Ox.My.MetaBox`. We're passing our
|
||||||
|
own `self`, so the `self.$boxes` that the `invert` method of `Ox.My.MetaBox`
|
||||||
|
operates on will be the PixelBoxes that we are assinging in the asynchronous
|
||||||
|
callback above.
|
||||||
|
|
||||||
|
This is somewhat analogous to the
|
||||||
|
<pre>
|
||||||
|
someOtherObject.method.apply(this, args)
|
||||||
|
</pre>
|
||||||
|
pattern that is common in JavaScript.
|
||||||
|
|
||||||
|
Note that we have to pass `self.options` too, otherwise our own size would
|
||||||
|
get overwritten by the MetaBox default size.
|
||||||
*/
|
*/
|
||||||
Ox.My.VideoBox = function(options, self) {
|
that.invert = Ox.My.MetaBox(self.options, self).invert;
|
||||||
|
|
||||||
};
|
/*
|
||||||
|
But wait — where's our `doubleclick` handler?
|
||||||
|
*/
|
||||||
|
return that;
|
||||||
|
|
||||||
(function() {
|
};
|
||||||
var h = Ox.random(360), s = 1, l = 0.5;
|
|
||||||
window.My = {};
|
/*
|
||||||
My.$backgroundBox = Ox.My.Box({
|
<hr>
|
||||||
size: [256, 256]
|
This one is left as an exercise to the reader ;)
|
||||||
})
|
*/
|
||||||
.append(
|
Ox.My.VideoBox = function(options, self) {
|
||||||
My.$box = Ox.My.Box({
|
|
||||||
color: Ox.rgb(h, s, l)
|
};
|
||||||
}),
|
|
||||||
My.$invertibleBox = Ox.My.InvertibleBox({
|
/*
|
||||||
color: Ox.rgb(h + 120, s, l)
|
<hr>
|
||||||
}),
|
Load the UI and Image modules.
|
||||||
My.$metaBox = Ox.My.MetaBox({
|
*/
|
||||||
color: Ox.range(2).map(function(y) {
|
Ox.load(['Image', 'UI'], function() {
|
||||||
return Ox.range(3).map(function(x) {
|
var h = Ox.random(360), s = 1, l = 0.5;
|
||||||
return Ox.rgb(h + x * 60 + y * 180, s, l);
|
window.My = {};
|
||||||
});
|
My.$backgroundBox = Ox.My.Box({
|
||||||
})
|
size: [256, 256]
|
||||||
}),
|
})
|
||||||
My.$pixelBox = Ox.My.PixelBox({
|
.append(
|
||||||
color: Ox.rgb(h + 120, s, l)
|
My.$box = Ox.My.Box({
|
||||||
|
color: Ox.rgb(h, s, l)
|
||||||
|
}),
|
||||||
|
My.$invertibleBox = Ox.My.InvertibleBox({
|
||||||
|
color: Ox.rgb(h + 120, s, l)
|
||||||
|
}),
|
||||||
|
My.$metaBox = Ox.My.MetaBox({
|
||||||
|
color: Ox.range(2).map(function(y) {
|
||||||
|
return Ox.range(3).map(function(x) {
|
||||||
|
return Ox.rgb(h + x * 60 + y * 180, s, l);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
)
|
}),
|
||||||
.appendTo(Ox.$body);
|
My.$pixelBox = Ox.My.PixelBox({
|
||||||
My.$imageBox = Ox.My.ImageBox({
|
color: Ox.rgb(h + 120, s, l)
|
||||||
image: 'png/pandora32.png',
|
|
||||||
size: [256, 256]
|
|
||||||
})
|
})
|
||||||
.appendTo(Ox.$body);
|
)
|
||||||
Ox.forEach(My, function($element, name) {
|
.appendTo(Ox.$body);
|
||||||
$element.bindEvent({
|
My.$imageBox = Ox.My.ImageBox({
|
||||||
invert: function() {
|
image: 'png/pandora32.png',
|
||||||
My.$box.displayText(name[1].toUpperCase() + name.slice(2));
|
size: [256, 256]
|
||||||
}
|
})
|
||||||
})
|
.appendTo(Ox.$body);
|
||||||
});
|
Ox.forEach(My, function($element, name) {
|
||||||
}());
|
$element.bindEvent({
|
||||||
|
invert: function() {
|
||||||
|
My.$box.displayText(name[1].toUpperCase() + name.slice(2));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
});
|
});
|
Loading…
Reference in a new issue