git.s-ol.nu mmm / f0b8e51
add aspect ratios s-ol 2 months ago
10 changed file(s) with 359 addition(s) and 1 deletion(s). Raw diff Collapse all Expand all
4343 else
4444 @canvas
4545
46
4746 update: (dt) =>
4847 @time += dt
4948
1 assert MODE == 'CLIENT', '[nossr]'
2
3 import CanvasApp from require 'mmm.canvasapp'
4 import div from require 'mmm.dom'
5
6 class UIDemo extends CanvasApp
7 width: nil
8 height: nil
9 new: () =>
10 super!
11
12 @canvas.width = nil
13 @canvas.height = nil
14 @canvas.style.width = '100%'
15 @canvas.style.height = '100%'
16 @canvas.style.border = '2px solid var(--gray-dark)'
17
18 @node = div @canvas, style: {
19 position: 'relative'
20 resize: 'horizontal'
21 overflow: 'hidden'
22
23 width: '576px'
24 height: '324px'
25 minWidth: '324px'
26 maxWidth: '742px'
27
28 margin: 'auto'
29 padding: '10px'
30 boxSizing: 'border-box'
31 }
32
33 -- match size of current parent element and return it (for interactive resizing in the demo)
34 updateSize: =>
35 { :clientWidth, :clientHeight } = @canvas.parentElement
36 @canvas.width, @canvas.height = clientWidth, clientHeight
37 @canvas.width, @canvas.height
38
39 -- set up a coordinate system such that the virtual viewport
40 -- of size (w, h) is centered on (0,0) and fills the canvas
41 -- returns remaining margins on the two axes
42 fit: (w, h) =>
43 width, height = @updateSize!
44 @ctx\translate width/2, height/2
45
46 -- maximum scale without cropping either axis
47 scale = math.min (width/w), (height/h)
48 @ctx\scale scale, scale
49
50 -- calculate remaining space on x/y axis
51 rx = (width/scale) - w
52 ry = (height/scale) - h
53
54 -- margins are half of the remaining space
55 rx / 2, ry / 2
56
57 strokeRect: (cx, cy, w, h) =>
58 lw = @ctx.lineWidth / 2
59 @ctx\strokeRect cx - w/2 + lw, cy - h/2 + lw,
60 w - 2*lw, h - 2*lw,
61
62 class Box
63 new: (@cx, @cy, @w, @h) =>
64
65 rect: =>
66 @cx, @cy, @w, @h
67
68 class Example extends UIDemo
69 click: =>
70 if @naive
71 @naive = false
72 else
73 if @show_boxes
74 @naive = true
75 @show_boxes = not @show_boxes
76
77 text: (box, text, align='center', my=.5) =>
78 mx = .1
79 @ctx.textAlign = align
80
81 if align == 'left'
82 @ctx\fillText text, box.cx + mx - box.w/2, box.cy + my
83 else if align == 'center'
84 @ctx\fillText text, box.cx, box.cy + my
85 if align == 'right'
86 @ctx\fillText text, box.cx - mx + box.w/2, box.cy + my
87
88 {
89 :UIDemo
90 :Example
91 :Box
92 }
1 =>
2 assert MODE == 'CLIENT', '[nossr]'
3
4 import UIDemo from @get '_base: table'
5
6 class FitDemo extends UIDemo
7 draw: =>
8 @fit 16, 9
9 @ctx.fillStyle = 'red'
10 @ctx\fillRect -8, -4.5, 16, 9
11
12 @ctx.fillStyle = 'black'
13 @ctx.font = '6px Arial'
14 @ctx.textAlign = 'center'
15 @ctx\fillText '16:9', 0, 2
16
17 FitDemo!
1 =>
2 assert MODE == 'CLIENT', '[nossr]'
3
4 import UIDemo from @get '_base: table'
5
6 arr = (args) ->
7 with js.new js.global.Array
8 for i in *args
9 \push i
10
11 class PerforateDemo extends UIDemo
12 draw: =>
13 @fit 16, 9
14
15 @ctx.lineWidth = 0.15
16 @ctx.strokeStyle = 'green'
17 @strokeRect 0, -3.5, 16, 2
18 @strokeRect 0, 3.5, 16, 2
19
20 @ctx\setLineDash arr { 0.4 }
21 @ctx\beginPath!
22 @ctx\moveTo 0, -4.5
23 @ctx\lineTo 0, -2.5
24 @ctx\moveTo 0, 2.5
25 @ctx\lineTo 0, 4.5
26 @ctx\stroke!
27 @ctx\setLineDash arr {}
28
29 @ctx.strokeStyle = 'blue'
30 @strokeRect 0, 0, 16, 5
31
32 @ctx.font = '4px Arial'
33 @ctx.textAlign = 'center'
34 @ctx\fillText '16:5', 0, 1.5
35
36 PerforateDemo!
1 =>
2 assert MODE == 'CLIENT', '[nossr]'
3
4 import Box, Example from @get '_base: table'
5
6 class Sidebar extends Example
7 draw: =>
8 margin_x, margin_y = @fit 16, 9
9 if @naive
10 margin_x, margin_y = 0, 0
11
12 @ctx.font = '1.5px Arial'
13 sidebar = Box -7 - margin_x, -1, 2, 7
14 @text sidebar, 'A', 'center', -1.8
15 @text sidebar, 'B', 'center', -0.3
16 @text sidebar, 'C', 'center', 1.2
17 @text sidebar, 'D', 'center', 2.7
18
19 bottom_l = Box -4 - margin_x, 3.5 + margin_y, 8, 2
20 bottom_r = Box 4 + margin_x, 3.5 + margin_y, 8, 2
21
22 @text bottom_l, 'levelname', 'left'
23 @text bottom_r, 'info a b c', 'right'
24
25 main = Box 1, -1, 14, 7
26 @ctx.lineWidth = 0.1
27 @ctx.strokeStyle = 'black'
28 @ctx\beginPath!
29 for x=-5.5, 7.5
30 @ctx\moveTo x, -4.5
31 @ctx\lineTo x, 2.5
32
33 for y=-4, 2
34 @ctx\moveTo -6, y
35 @ctx\lineTo 8, y
36 @ctx\stroke!
37
38 if @show_boxes
39 @ctx.lineWidth = 0.1
40 @ctx.strokeStyle = 'green'
41 @strokeRect sidebar\rect!
42 @strokeRect bottom_l\rect!
43 @strokeRect bottom_r\rect!
44
45 @ctx.strokeStyle = 'blue'
46 @strokeRect main\rect!
47
48 Sidebar!
1 =>
2 assert MODE == 'CLIENT', '[nossr]'
3
4 import UIDemo from @get '_base: table'
5
6 arr = (args) ->
7 with js.new js.global.Array
8 for i in *args
9 \push i
10
11 class TearDemo extends UIDemo
12 draw: =>
13 margin_x, margin_y = @fit 16, 9
14
15 @ctx.lineWidth = 0.15
16 @ctx.strokeStyle = 'green'
17 @strokeRect -4 - margin_x, -3.5 - margin_y, 8, 2
18 @strokeRect 4 + margin_x, -3.5 - margin_y, 8, 2
19
20 @strokeRect -4 - margin_x, 3.5 + margin_y, 8, 2
21 @strokeRect 4 + margin_x, 3.5 + margin_y, 8, 2
22
23 @ctx.strokeStyle = 'blue'
24 @strokeRect 0, 0, 16, 5
25
26 @ctx.font = '4px Arial'
27 @ctx.textAlign = 'center'
28 @ctx\fillText '16:5', 0, 1.5
29
30 TearDemo!
1 =>
2 assert MODE == 'CLIENT', '[nossr]'
3
4 import Box, Example from @get '_base: table'
5
6 class VTK extends Example
7 draw: =>
8 margin_x, margin_y = @fit 16, 9
9 if @naive
10 margin_x, margin_y = 0, 0
11
12 levelname = Box -4 - margin_x, -3.5 - margin_y, 8, 2
13 settings = Box 4 + margin_x, -3.5 - margin_y, 8, 2
14 infobar = Box -4 - margin_x, 3.5 + margin_y, 8, 2
15 exit = Box 4 + margin_x, 3.5 + margin_y, 8, 2
16
17 main = Box 0, 0, 16, 5
18
19 @ctx.font = '1.5px Arial'
20 @text levelname, 'levelname', 'left'
21 @text infobar, 'info a b c', 'left'
22 @text settings, 'settings', 'right'
23 @text exit, 'exit', 'right'
24
25 @ctx.lineWidth = 0.2
26 @ctx.strokeStyle = 'black'
27 @ctx\beginPath!
28 @ctx\moveTo -8 - margin_x, -2.5 - margin_y
29 @ctx\lineTo 8 + margin_x, -2.5 - margin_y
30 @ctx\moveTo -8 - margin_x, 2.5 + margin_y
31 @ctx\lineTo 8 + margin_x, 2.5 + margin_y
32 @ctx\stroke!
33
34 @ctx.lineWidth = 0.1
35 @ctx.strokeStyle = 'gray'
36 @ctx\beginPath!
37 for x=-7.5, 7.5
38 @ctx\moveTo x, -2.5
39 @ctx\lineTo x, 2.5
40
41 for y=-2, 2
42 @ctx\moveTo -8, y
43 @ctx\lineTo 8, y
44 @ctx\stroke!
45
46 if @show_boxes
47 @ctx.lineWidth = 0.1
48 @ctx.strokeStyle = 'green'
49 @strokeRect levelname\rect!
50 @strokeRect settings\rect!
51 @strokeRect infobar\rect!
52 @strokeRect exit\rect!
53
54 @ctx.strokeStyle = 'blue'
55 @strokeRect main\rect!
56
57 VTK!
1 Dealing with different screen sizes and formats can be annoying.
2 On desktop PCs, 16:9 is the most common aspect ratio these days, but depending on where you are rendering your content,
3 it might still be a different format, for example when a task bar, broswer navigation bar or similar is slicing away some of your screen real-estate.
4 For mobile games the situation is a bit worse, because there are simply more different aspect ratios out there
5 and different systems may require extra space for on-screen navigation keys, statusbars etc.
6
7 In this post I want to present a simple technique for layouting **simple UIs for games** that can work across a range of similar screens.
8 If you are looking for a way to design a UI-heavy app, or something that needs to work across very different environments (like phones, tablets and desktop PCs), this is probably not what you are looking for.
9 In any case, if you just want to take a quick look you can jump down to the [examples](#examples) at the bottom of the page as well.
10
11 ## step one: `fit`
12 The most trivial solution is to simply choose the aspect ratio you would like to work with,
13 and then fit a rectangle with that ratio into whichever space is available.
14 Depending on the screen, this may either be a perfect fit, leave space on the horizontal axis, or leave space on the vertical axis:
15 (drag the lower right corner to see this approach react to different screen sizes)
16
17 <mmm-embed path="interactive" facet="fit" nolink></mmm-embed>
18
19 This is pretty trivial to accomplish in code.
20 You can calculate the scales in relation to your reference grid on the x and y axis separately, and simply use the one with a lower value.
21 Establishing a reference grid and knowing the scale to translate between it and the physical screen sizes will also help
22 designing the layout in general, as we will see later.
23 Here is a simple example in JS:
24
25 ```js
26 // measured from somewhere
27 const width = 2000;
28 const height = 1080;
29
30 const sx = width / 16;
31 const sy = height / 9;
32
33 const scale = Math.min(sx, sy);
34 const offset_x = width * (1 - scale);
35 const offset_y = height * (1 - scale);
36
37 // the biggest 16:9 rectangle you can fit is (scale*width, scale*height) large
38 // and it's top-left corner is at (offset_x/2, offset_y/2)
39 ```
40
41 The problem with this is that it simply doesn't look very good. When the ratio is a perfect match there is of course no problem,
42 but otherwise the empty space makes the screen look empty (especially if there are system UI elements next to it).
43
44 ## step two: `perforate`
45 So how can this be imroved? We would like to use the unused space on the x or y axis, but we can't just scale everything up,
46 or we would start cropping important pieces of UI. Stretching the game to fill the screen also doesn't work for obvious reasons.
47
48 To proceed, the UI has to be 'perforated' into different sections that are independent from each other.
49 This is where establishing a reference grid becomes useful to orient ourselves in the layout.
50 In my example I am splitting the screen into a main content section that is 16:5 units large, as well as a top and bottom bar.
51 The two bars are also split in half vertically in the middle, for reasons that we will see in the third step.
52
53 <mmm-embed path="interactive" facet="perforate" nolink></mmm-embed>
54
55 It's imortant to note that how you divide the screen up depends completely on your game/interface of course.
56 The layout I am using here is just an example; at the end of this post you can find another one with a different layout as well.
57
58 ## step three: `tear`
59
60 Now that the sections are defined, in the last step we can 'tear' the sections apart and decide how they should react to the
61 left-over space calculated in step one individiually.
62 In my layout, I stretch the top and bottom bars to fill the screen completely horizontally.
63 By dividing the bars into separate sections in the last step, I know how much space is guaranteed to be available on each side.
64 If there is room left on the vertical axis, I move the bars out from the center to give the content some visual space:
65
66 <mmm-embed path="interactive" facet="tear" nolink></mmm-embed>
67
68 Once again, how you make the pieces behave depends a lot on what elements your UI has in the first place, and how you want it to look and feel.
69
70 # examples
71
72 Finally, here are two examples with a bit more visual coherence to show how this actually ends up working.
73 You can click on these to cycle between the normal view, showing the frames used to subdivide the canvas, and viewing `fit` only for comparison.
74
75 <mmm-embed path="interactive" facet="vtk" nolink></mmm-embed>
76 <mmm-embed path="interactive" facet="sidebar" nolink></mmm-embed>
77
1 Aspect-ratio independent UIs