'), $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($('
').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.
+ Public methods should return `that`, for chaining.
*/
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();
- }
- });
+/*
+
+Now we can "subclass" our Box. Let's build one that can have its color inverted.
+*/
+Ox.My.InvertibleBox = function(options, self) {
- self.options.inverted && setColor();
+ self = self || {};
+ /*
+ We no longer inherit from Ox.Element, but from `Ox.My.Box`.
+ We could have written
+
+ var that = Ox.My.Box({}, self)
+ .defaults({
+ color: [128, 128, 128],
+ inverted: false,
+ size: [128, 128]
+ })
+ .options(options || ())
+ .update({
+ ...
+ })
+
+ — 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:
+
+ .on({
+ click: function() {
+ that.invert();
+ }
+ })
+
+ */
+ .bindEvent({
+ doubleclick: function() {
+ that.invert();
+ }
+ });
- function getInvertedColor() {
- return self.options.color.map(function(value) {
+ /*
+ 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;
- });
- }
+ }) : self.options.color
+ ).join(', ') + ')'});
+ return false;
+ }
- function setColor() {
- that.css({backgroundColor: 'rgb(' + (
- self.options.inverted ? getInvertedColor() : self.options.color
- ).join(', ') + ')'});
+ /*
+ The public `invert` method is added as a convenience for the users of our
+ widget, so that when they want to toggle its inverted state, they don't have
+ to write
+
+ $widget.options({
+ inverted: !$widget.options('inverted')
+ });
+
+ all the time.
+
+ Also, we trigger an `invert` event, that anyone can bind to via
+
+ $widget.bindEvent({
+ invert: function() { ... }
+ });
+
+ */
+ that.invert = function() {
+ that.options({inverted: !self.options.inverted});
+ that.triggerEvent('invert');
+ return that;
+ };
+
+ /*
+ And again, we return `that`.
+ */
+ return that;
+
+};
+
+/*
+
+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:
+
+ Ox.My.MetaBox({
+ color: [
+ [[255, 0, 0], [255, 255, 0], [0, 255, 0]],
+ [[0, 255, 255], [0, 255, 0], [255, 0, 255]]
+ ]
+ });
+
+ */
+ 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;
+
+};
+
+/*
+
+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;
}
+ }
- that.invert = function() {
- that.options({inverted: !self.options.inverted});
- that.triggerEvent('invert');
- return that;
- };
-
- return that;
-
+ /*
+ Inverting a PixelBox is different from inverting a MetaBox, since we only
+ want to invert one color channel per box. This is where the shared private
+ `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
+ 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),
- 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) {
- return array.map(function(color, x) {
- return Ox.My.InvertibleBox({
- color: color,
+ self = self || {};
+ /*
+ Loading the image is asynchronous, but we want to display a box immediately.
+ 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]]
})
+ /*
+ ... remove its `doubleclick` handler ...
+ */
.unbindEvent('doubleclick')
+ /*
+ ... and append it to the ImageBox.
+ */
.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
+
+ someOtherObject.method.apply(this, args)
+
+ 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;
- (function() {
- var h = Ox.random(360), s = 1, l = 0.5;
- window.My = {};
- My.$backgroundBox = Ox.My.Box({
- size: [256, 256]
- })
- .append(
- 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);
- });
- })
- }),
- My.$pixelBox = Ox.My.PixelBox({
- color: Ox.rgb(h + 120, s, l)
+ /*
+ But wait — where's our `doubleclick` handler?
+ */
+ return that;
+
+};
+
+/*
+
+This one is left as an exercise to the reader ;)
+*/
+Ox.My.VideoBox = function(options, self) {
+
+};
+
+/*
+
+Load the UI and Image modules.
+*/
+Ox.load(['Image', 'UI'], function() {
+ var h = Ox.random(360), s = 1, l = 0.5;
+ window.My = {};
+ My.$backgroundBox = Ox.My.Box({
+ size: [256, 256]
+ })
+ .append(
+ 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.$imageBox = Ox.My.ImageBox({
- image: 'png/pandora32.png',
- size: [256, 256]
+ }),
+ My.$pixelBox = Ox.My.PixelBox({
+ color: Ox.rgb(h + 120, s, l)
})
- .appendTo(Ox.$body);
- Ox.forEach(My, function($element, name) {
- $element.bindEvent({
- invert: function() {
- My.$box.displayText(name[1].toUpperCase() + name.slice(2));
- }
- })
- });
- }());
-
-});
+ )
+ .appendTo(Ox.$body);
+ My.$imageBox = Ox.My.ImageBox({
+ image: 'png/pandora32.png',
+ 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));
+ }
+ })
+ });
+});
\ No newline at end of file