Begin encapsulating what I wrote in that gist. (Still has bugs.)
Chris Pressey
10 years ago
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>yoob.CanvasResizer Demo</title> | |
4 | <style> | |
5 | html, body, article { | |
6 | width: 100%; | |
7 | margin: 0; | |
8 | padding: 0; | |
9 | line-height: 0; | |
10 | text-align: center; | |
11 | } | |
12 | header { | |
13 | background: goldenrod; | |
14 | } | |
15 | </style> | |
16 | </head> | |
17 | <body> | |
18 | <header> | |
19 | <h1>yoob.Resizer Demo</h1> | |
20 | <p><button id='hide'>Hide header</button> (reload to bring it back)</p> | |
21 | </header> | |
22 | <article> | |
23 | <canvas id="c1"></canvas> | |
24 | </article> | |
25 | </body> | |
26 | <script src="../src/yoob/canvas-resizer.js"></script> | |
27 | <script type="text/javascript"> | |
28 | "use strict"; | |
29 | var canvas = document.getElementById('c1'); | |
30 | var ctx = c1.getContext('2d'); | |
31 | ||
32 | var cr = (new yoob.CanvasResizer()).init({ | |
33 | canvas: canvas, | |
34 | redraw: function() { | |
35 | ctx.fillStyle = 'red'; | |
36 | ctx.fillRect(0, 0, canvas.width / 2, canvas.height / 2); | |
37 | ctx.fillStyle = 'blue'; | |
38 | ctx.fillRect(canvas.width / 2, canvas.height / 2, canvas.width, canvas.height); | |
39 | }, | |
40 | desiredWidth: 640, | |
41 | desiredHeight: 400 | |
42 | }).register(); | |
43 | ||
44 | document.getElementById('hide').onclick = function() { | |
45 | document.getElementsByTagName('header')[0].style.display='none'; | |
46 | cr.resizeCanvas(); | |
47 | }; | |
48 | </script> |
0 | <!DOCTYPE html> | |
1 | <head> | |
2 | <meta charset="utf-8"> | |
3 | <title>yoob.Resizer Demo 1</title> | |
4 | <style> | |
5 | canvas { border: 1px solid blue; } | |
6 | </style> | |
7 | </head> | |
8 | <body> | |
9 | ||
10 | <h1>yoob.Resizer Demo 1</h1> | |
11 | ||
12 | <canvas id="c1"></canvas> | |
13 | ||
14 | <canvas id="c2"></canvas> | |
15 | ||
16 | </body> | |
17 | <script src="../src/yoob/resizer.js"></script> | |
18 | <script type="text/javascript"> | |
19 | "use strict"; | |
20 | var c1 = document.getElementById('c1'); | |
21 | var ctx1 = c1.getContext('2d'); | |
22 | var c2 = document.getElementById('c2'); | |
23 | var ctx2 = c2.getContext('2d'); | |
24 | ||
25 | var s1 = (new yoob.Size()).init({ width: 200, height: 400 }); | |
26 | s1.applyToElement(c1); | |
27 | ||
28 | var s2 = (new yoob.Size()).init({ width: 200, height: 8 }); | |
29 | s2.applyToElement(c2); | |
30 | ||
31 | var img = new Image(); | |
32 | img.onload = function() { | |
33 | var is = (new yoob.Size()).setFromImage(img); | |
34 | is.fit(s1); | |
35 | ctx1.drawImage(img, 0, 0, is.width, is.height); | |
36 | var is = (new yoob.Size()).setFromImage(img); | |
37 | is.fit(s2); | |
38 | ctx2.drawImage(img, 0, 0, is.width, is.height); | |
39 | } | |
40 | img.src = 'charset8.png'; | |
41 | ||
42 | </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 | * NOTE: this still has bugs! | |
9 | * | |
10 | * This class provides objects that resize a canvas to fill (or be centered in) | |
11 | * an area in the viewport, with several options. | |
12 | * | |
13 | * See here for the main use cases I wanted to address: | |
14 | * https://gist.github.com/cpressey/0e2d7f8f9a9a28c863ec | |
15 | * | |
16 | * You don't really need this if all you want is a full-viewport canvas; | |
17 | * that's easy enough to do with CSS and a simple onresize handler (see | |
18 | * above article.) But it does accomodate that if you want. | |
19 | */ | |
20 | yoob.CanvasResizer = function() { | |
21 | /* | |
22 | * Initializes this CanvasResizer and returns it. Does not hook into | |
23 | * any DOM events, so generally you want to call .register() afterwards. | |
24 | * | |
25 | * `canvas`: the canvas to resize | |
26 | * `redraw`: a function that redraws the canvas after redimensioning | |
27 | * (optional; not needed if redimensionCanvas is false.) | |
28 | * `desired{Width,Height}`: the desired width and height of the canvas | |
29 | * `redimensionCanvas`: should we set the canvas's width and height | |
30 | * properties to the clientWidth and clientHeight of the element | |
31 | * after it has been resized? defaults to true. | |
32 | * `retainAspectRatio`: should we try to retain the aspect ratio | |
33 | * of the canvas after resizing? defaults to true. | |
34 | * `allowExpansion`: should we ever resize the canvas to a size larger | |
35 | * than the desired width & height? defaults to false. | |
36 | * `centerVertically`: should we apply a top margin to the canvas | |
37 | * element, to equal half the available space below it, after resizing | |
38 | * it? defaults to defaults to true. | |
39 | */ | |
40 | this.init = function(cfg) { | |
41 | this.canvas = cfg.canvas; | |
42 | this.redraw = cfg.redraw || function() {}; | |
43 | this.desiredWidth = cfg.desiredWidth || null; | |
44 | this.desiredHeight = cfg.desiredHeight || null; | |
45 | this.redimensionCanvas = cfg.redimensionCanvas === false ? false : true; | |
46 | this.retainAspectRatio = cfg.retainAspectRatio === false ? false : true; | |
47 | this.allowExpansion = !!cfg.allowExpansion; | |
48 | this.centerVertically = cfg.centerVertically === false ? false : true; | |
49 | return this; | |
50 | }; | |
51 | ||
52 | this.register = function(w) { | |
53 | var $this = this; | |
54 | var resizeCanvas = function(e) { | |
55 | $this.resizeCanvas(e); | |
56 | }; | |
57 | window.addEventListener("load", resizeCanvas); | |
58 | window.addEventListener("resize", resizeCanvas); | |
59 | // TODO: orientationchange? | |
60 | return this; | |
61 | }; | |
62 | ||
63 | /* | |
64 | * Returns a two-element list, containing the width and height of the | |
65 | * available space in the viewport, measured from the upper-left corner | |
66 | * of the given element. | |
67 | */ | |
68 | this.getAvailableSpace = function(elem) { | |
69 | var rect = elem.getBoundingClientRect(); | |
70 | var absTop = Math.round(rect.top + window.pageYOffset); | |
71 | var absLeft = Math.round(rect.left + window.pageXOffset); | |
72 | var html = document.documentElement; | |
73 | var availWidth = html.clientWidth - absLeft * 2; | |
74 | var availHeight = html.clientHeight - (absTop + absLeft * 2); | |
75 | return [availWidth, availHeight]; | |
76 | }; | |
77 | ||
78 | /* | |
79 | * Given a destination width and height, return the scaling factor | |
80 | * which is needed to scale the desired width and height to that | |
81 | * destination rectangle. | |
82 | */ | |
83 | this.getFitScale = function(destWidth, destHeight) { | |
84 | var widthFactor = this.desiredWidth / destWidth; | |
85 | var heightFactor = this.desiredHeight / destHeight; | |
86 | return 1 / Math.max(widthFactor, heightFactor); | |
87 | }; | |
88 | ||
89 | this.resizeCanvas = function() { | |
90 | var avail = this.getAvailableSpace(this.canvas.parentElement); | |
91 | var availWidth = avail[0]; | |
92 | var availHeight = avail[1]; | |
93 | var newWidth = availWidth; | |
94 | var newHeight = availHeight; | |
95 | if (this.preserveAspectRatio) { | |
96 | var scale = this.getFitScale(avail); | |
97 | if (!this.allowExpansion) { | |
98 | scale = Math.min(scale, 1); | |
99 | } | |
100 | newWidth = Math.trunc(this.desiredWidth * scale); | |
101 | newHeight = Math.trunc(this.desiredHeight * scale); | |
102 | } else if (!this.allowExpansion) { | |
103 | // if we don't care about preserving the aspect ratio but do | |
104 | // care about preserving the maximum size, clamp each dimension | |
105 | newWidth = Math.min(newWidth, this.desiredWidth); | |
106 | newHeight = Math.min(newHeight, this.desiredHeight); | |
107 | } | |
108 | if (true) { | |
109 | // TODO: add an option to skip this part...? | |
110 | // you might want to skip it if you have these as %'s | |
111 | this.canvas.style.width = newWidth + "px"; | |
112 | this.canvas.style.height = newHeight + "px"; | |
113 | } | |
114 | if (this.centerVertically) { | |
115 | this.canvas.style.marginTop = "0"; | |
116 | if (availHeight > newHeight) { | |
117 | this.canvas.style.marginTop = | |
118 | Math.trunc((availHeight - newHeight) / 2) + "px"; | |
119 | } | |
120 | } | |
121 | if (this.redimensionCanvas) { | |
122 | if (this.canvas.width !== newWidth || this.canvas.height !== newHeight) { | |
123 | this.canvas.width = newWidth; | |
124 | this.canvas.height = newHeight; | |
125 | this.redraw(); | |
126 | } | |
127 | } | |
128 | }; | |
129 | }; |
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 | * These classes are provided to help craft solutions to resizing problems, | |
9 | * which (in my experience, anyway) are always rather nasty and ugly. | |
10 | * | |
11 | * First, we have a class which is an abstraction of a size. It may be | |
12 | * used explicitly to specify a desired size, or it may be used to determine | |
13 | * the size of some object (perhaps an Image, or a DOM element, or the | |
14 | * visible portion of a proposed DOM element.) | |
15 | */ | |
16 | ||
17 | yoob.Size = function() { | |
18 | this.init = function(cfg) { | |
19 | this.width = cfg.width; | |
20 | this.height = cfg.height; | |
21 | return this; | |
22 | }; | |
23 | ||
24 | this.setFromImage = function(img) { | |
25 | this.width = img.width; | |
26 | this.height = img.height; | |
27 | return this; | |
28 | }; | |
29 | ||
30 | this.setFromElement = function(elem) { | |
31 | this.width = elem.clientWidth; | |
32 | this.height = elem.clientHeight; | |
33 | return this; | |
34 | }; | |
35 | ||
36 | this.applyToElement = function(elem) { | |
37 | elem.width = this.width; | |
38 | elem.height = this.height; | |
39 | return this; | |
40 | }; | |
41 | ||
42 | this.applyAsStyle = function(elem) { | |
43 | elem.style.width = this.width + "px"; | |
44 | elem.style.height = this.height + "px"; | |
45 | return this; | |
46 | }; | |
47 | ||
48 | this.getAspectRatio = function() { | |
49 | return this.width / this.height; | |
50 | }; | |
51 | ||
52 | this.scale = function(factor) { | |
53 | this.width *= factor; | |
54 | this.height *= factor; | |
55 | return this; | |
56 | }; | |
57 | ||
58 | /* | |
59 | * Return the factor that would be required to scale this size by | |
60 | * in order to make it fit inside the given size, while preserving | |
61 | * the aspect ratio. | |
62 | */ | |
63 | this.getFitScale = function(destSize) { | |
64 | var widthFactor = this.width / destSize.width; | |
65 | var heightFactor = this.height / destSize.height; | |
66 | // say this is twice as wide, but 1.5 as high as dest. | |
67 | // then wf will be 2, and hf will be 1.5. | |
68 | // max of these is 2 | |
69 | // so fitscale will be 0.5 | |
70 | return 1 / Math.max(widthFactor, heightFactor); | |
71 | }; | |
72 | ||
73 | this.fit = function(destSize) { | |
74 | return this.scale(this.getFitScale(destSize)); | |
75 | }; | |
76 | ||
77 | /* | |
78 | * Assume this size is placed at (left, top) inside the given size. | |
79 | * Truncate this size so that it fits entirely within the given size. | |
80 | * For example, the given size might be window.client{Width,Height} | |
81 | * and the (left, top) might be the position of a div. | |
82 | */ | |
83 | this.clip = function(outerSize, left, top) { | |
84 | if (left + this.width > outerSize.width) { | |
85 | this.width = outerSize.width - left; | |
86 | } | |
87 | if (top + this.height > outerSize.height) { | |
88 | this.height = outerSize.height - top; | |
89 | } | |
90 | return this; | |
91 | }; | |
92 | }; | |
93 | ||
94 | /* | |
95 | * Offsets, too. | |
96 | */ | |
97 | ||
98 | yoob.Offset = function() { | |
99 | this.init = function(cfg) { | |
100 | this.left = cfg.left; | |
101 | this.top = cfg.top; | |
102 | return this; | |
103 | }; | |
104 | ||
105 | /* | |
106 | * Yep, this does that thing. | |
107 | */ | |
108 | this.setFromElement = function(elem) { | |
109 | var left = 0; | |
110 | var top = 0; | |
111 | while (elem) { | |
112 | left += elem.offsetLeft; | |
113 | top += elem.offsetTop; | |
114 | elem = elem.parentElement; | |
115 | } | |
116 | this.left = left; | |
117 | this.top = top; | |
118 | return this; | |
119 | }; | |
120 | ||
121 | this.applyAsStyle = function(elem) { | |
122 | elem.style.left = this.left + "px"; | |
123 | elem.style.top = this.top + "px"; | |
124 | return this; | |
125 | }; | |
126 | }; | |
127 | ||
128 | /* | |
129 | * Now that we have Sizes, what we want to do is reconcile them | |
130 | * | |
131 | * So, what we have is a _source size_, which might be the size of an image we | |
132 | * wish to display, or the extents of a playfield we wish to consider. | |
133 | * | |
134 | * We also have a _destination size_, which might be the available screen | |
135 | * real estate for displaying the source image. Or, more precisely, it | |
136 | * might be the available size, in a given DOM element, taking account of | |
137 | * any number of factors, like visibility on the screen. | |
138 | * | |
139 | * From these, and various jiggery-pokery, we compute a _result size_. | |
140 | */ | |
141 | yoob.Resizer = function() { | |
142 | this.init = function(cfg) { | |
143 | this.src = cfg.src; // assumed to be a Size | |
144 | this.dest = cfg.dest; // assumed to be a Size | |
145 | return this; | |
146 | }; | |
147 | ||
148 | this.setSource = function(src) { this.src = src; return this; } | |
149 | this.setDestination = function(dest) { this.dest = dest; return this; } | |
150 | ||
151 | /* ??? */ | |
152 | /* 3. Profit! */ | |
153 | ||
154 | /* Things to handle: | |
155 | onload | |
156 | onresize | |
157 | onorientationchange | |
158 | if style.width/height expressed in %, re-get clientW/H after such | |
159 | */ | |
160 | }; |