git @ Cat's Eye Technologies HTML5-Gewgaws / 4647fd4
Import files for Black Hole Poem. Chris Pressey 10 years ago
4 changed file(s) with 680 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 "use strict";
1
2 function launch(prefix, containerId, config) {
3 var config = config || {};
4 var deps = [
5 "element-factory.js",
6 "animation.js",
7 "sprite-manager.js",
8 "canvas-resizer.js"
9 ];
10 var loaded = 0;
11 for (var i = 0; i < deps.length; i++) {
12 var elem = document.createElement('script');
13 elem.src = prefix + deps[i];
14 elem.onload = function() {
15 if (++loaded == deps.length) {
16 var container = document.getElementById(containerId);
17 var t = new BlackHolePoem();
18 var initialized = false;
19 config.canvas = yoob.makeCanvas(container, 800, 450);
20 var cr = (new yoob.CanvasResizer()).init({
21 canvas: config.canvas,
22 onResizeEnd: function() {
23 if (!initialized) {
24 t.init(config);
25 initialized = true;
26 }
27 t.draw();
28 },
29 desiredWidth: 800,
30 desiredHeight: 450
31 }).register();
32 }
33 };
34 document.body.appendChild(elem);
35 }
36 }
37
38 var makeText = function(cfg) {
39 var sprite = (new yoob.Sprite()).init({
40 x: cfg.x,
41 y: cfg.y,
42 width: 30,
43 height: 30,
44 isDraggable: true
45 });
46
47 sprite.font = cfg.font || "64px Arial,Sans-serif";
48 sprite.text = cfg.text;
49 sprite.anchorX = cfg.anchorX;
50 sprite.anchorY = cfg.anchorY;
51
52 sprite.draw = function(ctx) {
53 ctx.fillStyle = "#432e2a";
54 ctx.fillRect(this.getLeftX(), this.getTopY(), this.getWidth(), this.getHeight());
55
56 ctx.textBaseline = "middle";
57 ctx.font = this.font;
58 ctx.fillStyle = "black";
59
60 var x = this.getX();
61 var endX = this.anchorX();
62 var y = this.getY();
63 var endY = this.anchorY();
64
65 for (var i = 0; i < this.text.length; i++) {
66 var c = this.text.charAt(i);
67 if (c===' ') continue;
68 var width = ctx.measureText(c).width;
69 var textX = x - width / 2;
70 ctx.fillText(c, textX, y);
71 x += (endX - x) / 2;
72 y += (endY - y) / 2;
73 }
74 };
75
76 cfg.manager.addSprite(sprite);
77 return sprite;
78 };
79
80 var BlackHolePoem = function() {
81 var canvas;
82 var ctx;
83 var manager;
84 var texts;
85
86 this.draw = function() {
87 // Illuminant E: #D3BEBA
88 ctx.fillStyle = "#816660";
89 ctx.fillRect(0, 0, canvas.width, canvas.height);
90 manager.draw(ctx);
91 };
92
93 this.update = function() {
94 };
95
96 this.init = function(config) {
97 canvas = config.canvas;
98 ctx = canvas.getContext("2d");
99
100 var anchorX = function() { return canvas.width / 2; };
101 var anchorY = function() { return canvas.height / 2; };
102
103 manager = (new yoob.SpriteManager()).init({ canvas: canvas });
104 texts = [];
105 texts.push(makeText({
106 text: "A billion light-years",
107 manager: manager,
108 x: 20,
109 y: 20,
110 anchorX: anchorX,
111 anchorY: anchorY
112 }));
113
114 texts.push(makeText({
115 text: "Distant and unseen",
116 manager: manager,
117 x: canvas.width - 20,
118 y: canvas.height - 20,
119 anchorX: anchorX,
120 anchorY: anchorY
121 }));
122
123 texts.push(makeText({
124 text: "Relative to nothing",
125 manager: manager,
126 x: 20,
127 y: canvas.height - 20,
128 anchorX: anchorX,
129 anchorY: anchorY
130 }));
131
132 texts.push(makeText({
133 text: "Unequalled forces",
134 manager: manager,
135 x: canvas.width - 20,
136 y: 20,
137 anchorX: anchorX,
138 anchorY: anchorY
139 }));
140
141 var $this = this;
142 $this.animation = (new yoob.Animation()).init({
143 object: $this
144 });
145 $this.animation.start();
146 };
147 };
0 <!DOCTYPE html>
1 <head>
2 <meta charset="utf-8">
3 <title>Black Hole Poem</title>
4 <meta name="viewport" content="width=device-width, initial-scale=1.0">
5 <style>
6 html, body, div {
7 width: 100%;
8 text-align: center;
9 margin: 0;
10 padding: 0;
11 line-height: 0;
12 }
13 </style>
14 </head>
15 <body>
16
17 <h1>Black Hole Poem</h1>
18
19 <div id="container"></div>
20
21 </body>
22 <script src="black-hole-poem.js"></script>
23 <script>
24 launch('../common-yoob.js-0.9/', 'container');
25 </script>
0 /*
1 * This file is part of yoob.js version 0.9-PRE
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * This class provides objects that resize a canvas to fill (or be centered in)
9 * an area in the viewport, with several options.
10 *
11 * See here for the main use cases I wanted to address:
12 * https://gist.github.com/cpressey/0e2d7f8f9a9a28c863ec
13 *
14 * You don't really need this if all you want is a full-viewport canvas;
15 * that's easy enough to do with CSS and a simple onresize handler (see
16 * above article.) But it does accomodate that if you want.
17 */
18 yoob.CanvasResizer = function() {
19 /*
20 * Initializes this CanvasResizer and returns it. Does not hook into
21 * any DOM events, so generally you want to call .register() afterwards.
22 *
23 * `canvas`: the canvas to resize
24 *
25 * `onResizeStart`: an optional function which, if supplied, will be
26 * called, passing the new width and height as parameters, after the
27 * new canvas size has been computed, but before the canvas is actually
28 * resized. It may return the exact object `false` to cancel the resize.
29 *
30 * `onResizeEnd`: an optional function which, if supplied, will be
31 * called after the canvas has actually been resized. This can be
32 * used to, for example, redraw the canvas contents.
33 *
34 * `desired{Width,Height}`: the desired width and height of the canvas
35 *
36 * `redimensionCanvas`: should we set the canvas's width and height
37 * properties to the clientWidth and clientHeight of the element
38 * after it has been resized? defaults to true.
39 *
40 * `preserveAspectRatio`: should we try to preserve the aspect ratio
41 * of the canvas after resizing? defaults to true.
42 *
43 * `allowExpansion`: should we ever resize the canvas to a size larger
44 * than the desired width & height? defaults to false.
45 *
46 * `allowContraction`: should we ever resize the canvas to a size smaller
47 * than the desired width & height? defaults to true.
48 *
49 * `missingCanvasElement`: if allowContraction is false, this should be
50 * a DOM element whose `display` style will be changed from `none` to
51 * `inline-block` when the viewport is too small to display the canvas.
52 *
53 * `centerVertically`: should we apply a top margin to the canvas
54 * element, to equal half the available space below it, after resizing
55 * it? defaults to defaults to true.
56 */
57 this.init = function(cfg) {
58 var nop = function() {};
59 this.canvas = cfg.canvas;
60 this.redraw = cfg.redraw || nop;
61 this.onResizeStart = cfg.onResizeStart || nop;
62 this.onResizeEnd = cfg.onResizeEnd || nop;
63 this.desiredWidth = cfg.desiredWidth || null;
64 this.desiredHeight = cfg.desiredHeight || null;
65 this.redimensionCanvas = cfg.redimensionCanvas === false ? false : true;
66 this.preserveAspectRatio = cfg.preserveAspectRatio === false ? false : true;
67 this.allowExpansion = !!cfg.allowExpansion;
68 this.allowContraction = cfg.allowContraction === false ? false : true;
69 this.missingCanvasElement = cfg.missingCanvasElement;
70 this.centerVertically = cfg.centerVertically === false ? false : true;
71 return this;
72 };
73
74 this.register = function(w) {
75 var $this = this;
76 var resizeCanvas = function(e) {
77 $this.resizeCanvas(e);
78 };
79 window.addEventListener("load", resizeCanvas);
80 window.addEventListener("resize", resizeCanvas);
81 // TODO: orientationchange?
82 return this;
83 };
84
85 /*
86 * Returns a two-element list, containing the width and height of the
87 * available space in the viewport, measured from the upper-left corner
88 * of the given element.
89 */
90 this.getAvailableSpace = function(elem) {
91 var rect = elem.getBoundingClientRect();
92 var absTop = Math.round(rect.top + window.pageYOffset);
93 var absLeft = Math.round(rect.left + window.pageXOffset);
94 var html = document.documentElement;
95 var availWidth = html.clientWidth - absLeft * 2;
96 var availHeight = html.clientHeight - (absTop + absLeft * 2);
97 return [availWidth, availHeight];
98 };
99
100 /*
101 * Given a destination width and height, return the scaling factor
102 * which is needed to scale the desired width and height to that
103 * destination rectangle.
104 */
105 this.getFitScale = function(destWidth, destHeight) {
106 var widthFactor = this.desiredWidth / destWidth;
107 var heightFactor = this.desiredHeight / destHeight;
108 return 1 / Math.max(widthFactor, heightFactor);
109 };
110
111 this.resizeCanvas = function() {
112 var avail = this.getAvailableSpace(this.canvas.parentElement);
113 var availWidth = avail[0];
114 var availHeight = avail[1];
115 var newWidth = availWidth;
116 var newHeight = availHeight;
117
118 if (this.preserveAspectRatio) {
119 var scale = this.getFitScale(availWidth, availHeight);
120 if (!this.allowExpansion) {
121 scale = Math.min(scale, 1);
122 }
123 if (!this.allowContraction) {
124 scale = Math.max(scale, 1);
125 }
126 newWidth = Math.trunc(this.desiredWidth * scale);
127 newHeight = Math.trunc(this.desiredHeight * scale);
128 } else {
129 // if we don't care about preserving the aspect ratio but do
130 // care about preserving the size, clamp each dimension
131 if (!this.allowExpansion) {
132 newWidth = Math.min(newWidth, this.desiredWidth);
133 newHeight = Math.min(newHeight, this.desiredHeight);
134 }
135 if (!this.allowContraction) {
136 newWidth = Math.max(newWidth, this.desiredWidth);
137 newHeight = Math.max(newHeight, this.desiredHeight);
138 }
139 }
140
141 if (newWidth > availWidth || newHeight > availHeight) {
142 // due to not allowing contraction, our canvas is still
143 // too big to display. hide it and show the other thing
144 this.canvas.style.display = 'none';
145 if (this.missingCanvasElement) {
146 this.missingCanvasElement.style.display = 'inline-block';
147 }
148 return;
149 }
150
151 this.canvas.style.display = 'inline-block';
152 if (this.missingCanvasElement) {
153 this.missingCanvasElement.style.display = 'none';
154 }
155
156 var result = this.onResizeStart(newWidth, newHeight);
157 if (result === false) {
158 return;
159 }
160
161 if (true) {
162 // TODO: add an option to skip this part...?
163 // you might want to skip it if you have these as %'s
164 this.canvas.style.width = newWidth + "px";
165 this.canvas.style.height = newHeight + "px";
166 }
167
168 this.canvas.style.marginTop = "0";
169 if (this.centerVertically) {
170 if (availHeight > newHeight) {
171 this.canvas.style.marginTop =
172 Math.trunc((availHeight - newHeight) / 2) + "px";
173 }
174 }
175
176 var changed = false;
177 if (this.redimensionCanvas) {
178 if (this.canvas.width !== newWidth || this.canvas.height !== newHeight) {
179 this.canvas.width = newWidth;
180 this.canvas.height = newHeight;
181 changed = true;
182 }
183 }
184
185 this.onResizeEnd(newWidth, newHeight, changed);
186 };
187 };
0 /*
1 * This file is part of yoob.js version 0.9-PRE
2 * Available from https://github.com/catseye/yoob.js/
3 * This file is in the public domain. See http://unlicense.org/ for details.
4 */
5 if (window.yoob === undefined) yoob = {};
6
7 /*
8 * This is really just an interface; duck-typing-wise, you can use any
9 * Javascript object you want as a sprite, so long as it exposes these
10 * methods.
11 */
12 yoob.Sprite = function() {
13
14 /*
15 * x and y always represent the CENTRE of the Sprite().
16 * Chainable.
17 */
18 this.init = function(cfg) {
19 this.x = cfg.x;
20 this.y = cfg.y;
21 this.width = cfg.width;
22 this.height = cfg.height;
23 this.dx = cfg.dx || 0;
24 this.dy = cfg.dy || 0;
25 this.isDraggable = cfg.isDraggable || false;
26 this.isClickable = cfg.isClickable || false;
27 this.fillStyle = cfg.fillStyle || "green";
28 this.visible = (cfg.visible === undefined ? true : (!!cfg.visible));
29 this._isBeingDragged = false;
30 return this;
31 };
32
33 this.getX = function() {
34 return this.x;
35 };
36
37 this.getLeftX = function() {
38 return this.x - this.width / 2;
39 };
40
41 this.getRightX = function() {
42 return this.x + this.width / 2;
43 };
44
45 this.getY = function() {
46 return this.y;
47 };
48
49 this.getTopY = function() {
50 return this.y - this.height / 2;
51 };
52
53 this.getBottomY = function() {
54 return this.y + this.height / 2;
55 };
56
57 this.getWidth = function() {
58 return this.width;
59 };
60
61 this.getHeight = function() {
62 return this.height;
63 };
64
65 this.isBeingDragged = function() {
66 return this._isBeingDragged;
67 };
68
69 /*
70 * Chainable.
71 */
72 this.setPosition = function(x, y) {
73 this.x = x;
74 this.y = y;
75 return this;
76 };
77
78 /*
79 * Chainable.
80 */
81 this.setDimensions = function(width, height) {
82 this.width = width;
83 this.height = height;
84 return this;
85 };
86
87 /*
88 * Chainable.
89 */
90 this.setVelocity = function(dx, dy) {
91 this.dx = dx;
92 this.dy = dy;
93 return this;
94 };
95
96 /*
97 * Chainable.
98 */
99 this.setDestination = function(x, y, ticks) {
100 this.destX = x;
101 this.destY = y;
102 this.setVelocity((this.destX - this.getX()) / ticks, (this.destY - this.getY()) / ticks);
103 this.destCounter = ticks;
104 return this;
105 };
106
107 this.move = function(x, y) {
108 this.x += this.dx;
109 this.y += this.dy;
110 this.onmove();
111 if (this.destCounter !== undefined) {
112 this.destCounter--;
113 if (this.destCounter <= 0) {
114 this.destCounter = undefined;
115 this.setPosition(this.destX, this.destY).setVelocity(0, 0);
116 this.onreachdestination();
117 }
118 }
119 };
120
121 // override this if your shape is not a rectangle
122 this.containsPoint = function(x, y) {
123 return (x >= this.getLeftX() && x <= this.getRightX() &&
124 y >= this.getTopY() && y <= this.getBottomY());
125 };
126
127 // you may need to override this in a sophisticated way if you
128 // expect it to detect sprites of different shapes intersecting
129 this.intersects = function(sprite) {
130 var x1 = this.getLeftX();
131 var x2 = this.getRightX();
132 var y1 = this.getTopY();
133 var y2 = this.getBottomY();
134 return (sprite.containsPoint(x1, y1) || sprite.containsPoint(x2, y1) ||
135 sprite.containsPoint(x1, y2) || sprite.containsPoint(x2, y2));
136 };
137
138 // you will probably want to override this
139 // if you do, it's up to you to honour this.visible.
140 this.draw = function(ctx) {
141 if (!this.visible) return;
142 ctx.fillStyle = this.fillStyle || "green";
143 ctx.fillRect(this.getLeftX(), this.getTopY(), this.getWidth(), this.getHeight());
144 };
145
146 // event handlers. override to detect these events.
147 this.ongrab = function() {
148 };
149 this.ondrag = function() {
150 };
151 this.ondrop = function() {
152 };
153 this.onclick = function() {
154 };
155 this.onmove = function() {
156 };
157 this.onreachdestination = function() {
158 };
159
160 };
161
162 /*
163 * This still has a few shortcomings at the moment.
164 */
165 yoob.SpriteManager = function() {
166 /*
167 * Attach this SpriteManager to a canvas.
168 */
169 this.init = function(cfg) {
170 this.canvasX = undefined;
171 this.canvasY = undefined;
172 this.offsetX = undefined;
173 this.offsetY = undefined;
174 this.dragging = undefined;
175 this.sprites = [];
176
177 this.canvas = cfg.canvas;
178
179 var $this = this;
180 this.canvas.addEventListener('mousedown', function(e) {
181 return $this.onmousedown(e, e);
182 });
183 this.canvas.addEventListener('touchstart', function(e) {
184 return $this.onmousedown(e, e.touches[0]);
185 });
186
187 this.canvas.addEventListener('mousemove', function(e) {
188 return $this.onmousemove(e, e);
189 });
190 this.canvas.addEventListener('touchmove', function(e) {
191 return $this.onmousemove(e, e.touches[0]);
192 });
193
194 this.canvas.addEventListener('mouseup', function(e) {
195 return $this.onmouseup(e, e);
196 });
197 this.canvas.addEventListener('touchend', function(e) {
198 return $this.onmouseup(e, e.touches[0]);
199 });
200
201 return this;
202 };
203
204 /*
205 * Common handling of mouse and touch events
206 */
207 this.onmousedown = function(e, touch) {
208 e.preventDefault();
209 this.canvasX = touch.pageX - this.canvas.offsetLeft;
210 this.canvasY = touch.pageY - this.canvas.offsetTop;
211
212 var sprite = this.getSpriteAt(this.canvasX, this.canvasY);
213 if (sprite === undefined) return;
214 if (sprite.isDraggable) {
215 this.dragging = sprite;
216 this.dragging._isBeingDragged = true;
217 this.dragging.ongrab();
218 this.offsetX = sprite.getX() - this.canvasX;
219 this.offsetY = sprite.getY() - this.canvasY;
220 this.canvas.style.cursor = "move";
221 } else if (sprite.isClickable) {
222 sprite.onclick(e);
223 }
224 };
225
226 this.onmousemove = function(e, touch) {
227 e.preventDefault();
228 if (!this.dragging) return;
229
230 this.canvasX = touch.pageX - this.canvas.offsetLeft;
231 this.canvasY = touch.pageY - this.canvas.offsetTop;
232
233 this.dragging.setPosition(this.canvasX + this.offsetX,
234 this.canvasY + this.offsetY);
235 };
236
237 this.onmouseup = function(e, touch) {
238 e.preventDefault();
239 this.canvas.onmousemove = null;
240 this.canvasX = undefined;
241 this.canvasY = undefined;
242 this.offsetX = undefined;
243 this.offsetY = undefined;
244 if (this.dragging !== undefined) {
245 this.dragging.ondrop();
246 this.dragging._isBeingDragged = false;
247 }
248 this.dragging = undefined;
249 this.canvas.style.cursor = "auto";
250 };
251
252 this.move = function() {
253 this.foreach(function(sprite) {
254 sprite.move();
255 });
256 };
257
258 this.draw = function(ctx) {
259 if (ctx === undefined) {
260 ctx = this.canvas.getContext('2d');
261 }
262 this.foreach(function(sprite) {
263 sprite.draw(ctx);
264 });
265 };
266
267 this.addSprite = function(sprite) {
268 this.sprites.push(sprite);
269 };
270
271 this.removeSprite = function(sprite) {
272 var index = undefined;
273 for (var i = 0; i < this.sprites.length; i++) {
274 if (this.sprites[i] === sprite) {
275 index = i;
276 break;
277 }
278 }
279 if (index !== undefined) {
280 this.sprites.splice(index, 1);
281 }
282 };
283
284 this.moveToFront = function(sprite) {
285 this.removeSprite(sprite);
286 this.sprites.push(sprite);
287 };
288
289 this.moveToBack = function(sprite) {
290 this.removeSprite(sprite);
291 this.sprites.unshift(sprite);
292 };
293
294 this.getSpriteAt = function(x, y) {
295 for (var i = this.sprites.length-1; i >= 0; i--) {
296 var sprite = this.sprites[i];
297 if (sprite.containsPoint(x, y)) {
298 return sprite;
299 }
300 }
301 return undefined;
302 };
303
304 this.foreach = function(fun) {
305 for (var i = this.sprites.length-1; i >= 0; i--) {
306 var sprite = this.sprites[i];
307 var result = fun(sprite);
308 if (result === 'remove') {
309 this.removeSprite(sprite);
310 }
311 if (result === 'return') {
312 return sprite;
313 }
314 }
315 };
316
317 };