Split root out of mmm repo
s-ol
1 year, 7 months ago
0 | about me | |
1 | ======== | |
2 | i am s-ol bekic, a designer and creative technologist currently based in milano. | |
3 | if you are looking for an overview over my work or skillset, take a look at my [portfolio][portfolio]. | |
4 | ||
5 | [portfolio]: /portfolio/ |
0 | humane_filesystems | |
1 | ludum_dare_33_postmortem | |
2 | video_synth_research | |
3 | love_lua_photoshop_and_games | |
4 | clocks_triggers_gates | |
5 | stretching_gates | |
6 | stencils_101 | |
7 | aspect_ratios | |
8 | automating_my_rice | |
9 | challenging_myself | |
10 | self-hosted_virtual_home | |
11 | why_redirectly |
0 | import div, a from require 'mmm.dom' | |
1 | import interactive_link from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | ||
3 | if MODE ~= 'CLIENT' | |
4 | class Dummy | |
5 | render: => | |
6 | div { | |
7 | style: | |
8 | position: 'relative' | |
9 | resize: 'horizontal' | |
10 | overflow: 'hidden' | |
11 | ||
12 | width: '480px' | |
13 | height: '270px' | |
14 | 'min-width': '270px' | |
15 | 'max-width': '100%' | |
16 | ||
17 | margin: 'auto' | |
18 | padding: '10px' | |
19 | boxSizing: 'border-box' | |
20 | background: 'var(--gray-bright)' | |
21 | ||
22 | interactive_link '(click here for the interactive version of this article)' | |
23 | } | |
24 | ||
25 | return { | |
26 | UIDemo: Dummy | |
27 | Example: Dummy | |
28 | } | |
29 | ||
30 | import CanvasApp from require 'mmm.canvasapp' | |
31 | ||
32 | class UIDemo extends CanvasApp | |
33 | width: nil | |
34 | height: nil | |
35 | new: () => | |
36 | super! | |
37 | ||
38 | @canvas.width = nil | |
39 | @canvas.height = nil | |
40 | @canvas.style.width = '100%' | |
41 | @canvas.style.height = '100%' | |
42 | @canvas.style.border = '2px solid var(--gray-dark)' | |
43 | ||
44 | @node = div @canvas, style: { | |
45 | position: 'relative' | |
46 | resize: 'horizontal' | |
47 | overflow: 'hidden' | |
48 | ||
49 | width: '480px' | |
50 | height: '270px' | |
51 | minWidth: '270px' | |
52 | maxWidth: '100%' | |
53 | ||
54 | margin: 'auto' | |
55 | padding: '10px' | |
56 | boxSizing: 'border-box' | |
57 | } | |
58 | ||
59 | -- match size of current parent element and return it (for interactive resizing in the demo) | |
60 | updateSize: => | |
61 | { :clientWidth, :clientHeight } = @canvas.parentElement | |
62 | @canvas.width, @canvas.height = clientWidth, clientHeight | |
63 | @canvas.width, @canvas.height | |
64 | ||
65 | -- set up a coordinate system such that the virtual viewport | |
66 | -- of size (w, h) is centered on (0,0) and fills the canvas | |
67 | -- returns remaining margins on the two axes | |
68 | fit: (w, h) => | |
69 | width, height = @updateSize! | |
70 | @ctx\translate width/2, height/2 | |
71 | ||
72 | -- maximum scale without cropping either axis | |
73 | scale = math.min (width/w), (height/h) | |
74 | @ctx\scale scale, scale | |
75 | ||
76 | -- calculate remaining space on x/y axis | |
77 | rx = (width/scale) - w | |
78 | ry = (height/scale) - h | |
79 | ||
80 | -- margins are half of the remaining space | |
81 | rx / 2, ry / 2 | |
82 | ||
83 | strokeRect: (cx, cy, w, h) => | |
84 | lw = @ctx.lineWidth / 2 | |
85 | @ctx\strokeRect cx - w/2 + lw, cy - h/2 + lw, | |
86 | w - 2*lw, h - 2*lw, | |
87 | ||
88 | class Box | |
89 | new: (@cx, @cy, @w, @h) => | |
90 | ||
91 | rect: => | |
92 | @cx, @cy, @w, @h | |
93 | ||
94 | class Example extends UIDemo | |
95 | click: => | |
96 | if @naive | |
97 | @naive = false | |
98 | else | |
99 | if @show_boxes | |
100 | @naive = true | |
101 | @show_boxes = not @show_boxes | |
102 | ||
103 | text: (box, text, align='center', my=.5) => | |
104 | mx = .1 | |
105 | @ctx.textAlign = align | |
106 | ||
107 | if align == 'left' | |
108 | @ctx\fillText text, box.cx + mx - box.w/2, box.cy + my | |
109 | else if align == 'center' | |
110 | @ctx\fillText text, box.cx, box.cy + my | |
111 | if align == 'right' | |
112 | @ctx\fillText text, box.cx - mx + box.w/2, box.cy + my | |
113 | ||
114 | { | |
115 | :UIDemo | |
116 | :Example | |
117 | :Box | |
118 | } |
+0
-15
0 | => | |
1 | import UIDemo from @get '_base: table' | |
2 | ||
3 | class FitDemo extends UIDemo | |
4 | draw: => | |
5 | @fit 16, 9 | |
6 | @ctx.fillStyle = 'red' | |
7 | @ctx\fillRect -8, -4.5, 16, 9 | |
8 | ||
9 | @ctx.fillStyle = 'black' | |
10 | @ctx.font = '6px Arial' | |
11 | @ctx.textAlign = 'center' | |
12 | @ctx\fillText '16:9', 0, 2 | |
13 | ||
14 | FitDemo! |
+0
-34
0 | => | |
1 | import UIDemo from @get '_base: table' | |
2 | ||
3 | arr = (args) -> | |
4 | with js.new js.global.Array | |
5 | for i in *args | |
6 | \push i | |
7 | ||
8 | class PerforateDemo extends UIDemo | |
9 | draw: => | |
10 | @fit 16, 9 | |
11 | ||
12 | @ctx.lineWidth = 0.15 | |
13 | @ctx.strokeStyle = 'green' | |
14 | @strokeRect 0, -3.5, 16, 2 | |
15 | @strokeRect 0, 3.5, 16, 2 | |
16 | ||
17 | @ctx\setLineDash arr { 0.4 } | |
18 | @ctx\beginPath! | |
19 | @ctx\moveTo 0, -4.5 | |
20 | @ctx\lineTo 0, -2.5 | |
21 | @ctx\moveTo 0, 2.5 | |
22 | @ctx\lineTo 0, 4.5 | |
23 | @ctx\stroke! | |
24 | @ctx\setLineDash arr {} | |
25 | ||
26 | @ctx.strokeStyle = 'blue' | |
27 | @strokeRect 0, 0, 16, 5 | |
28 | ||
29 | @ctx.font = '4px Arial' | |
30 | @ctx.textAlign = 'center' | |
31 | @ctx\fillText '16:5', 0, 1.5 | |
32 | ||
33 | PerforateDemo! |
+0
-46
0 | => | |
1 | import Box, Example from @get '_base: table' | |
2 | ||
3 | class Sidebar extends Example | |
4 | draw: => | |
5 | margin_x, margin_y = @fit 16, 9 | |
6 | if @naive | |
7 | margin_x, margin_y = 0, 0 | |
8 | ||
9 | @ctx.font = '1.5px Arial' | |
10 | sidebar = Box -7 - margin_x, -1, 2, 7 | |
11 | @text sidebar, 'A', 'center', -1.8 | |
12 | @text sidebar, 'B', 'center', -0.3 | |
13 | @text sidebar, 'C', 'center', 1.2 | |
14 | @text sidebar, 'D', 'center', 2.7 | |
15 | ||
16 | bottom_l = Box -4 - margin_x, 3.5 + margin_y, 8, 2 | |
17 | bottom_r = Box 4 + margin_x, 3.5 + margin_y, 8, 2 | |
18 | ||
19 | @text bottom_l, 'levelname', 'left' | |
20 | @text bottom_r, 'info a b c', 'right' | |
21 | ||
22 | main = Box 1, -1, 14, 7 | |
23 | @ctx.lineWidth = 0.1 | |
24 | @ctx.strokeStyle = 'black' | |
25 | @ctx\beginPath! | |
26 | for x=-5.5, 7.5 | |
27 | @ctx\moveTo x, -4.5 | |
28 | @ctx\lineTo x, 2.5 | |
29 | ||
30 | for y=-4, 2 | |
31 | @ctx\moveTo -6, y | |
32 | @ctx\lineTo 8, y | |
33 | @ctx\stroke! | |
34 | ||
35 | if @show_boxes | |
36 | @ctx.lineWidth = 0.1 | |
37 | @ctx.strokeStyle = 'green' | |
38 | @strokeRect sidebar\rect! | |
39 | @strokeRect bottom_l\rect! | |
40 | @strokeRect bottom_r\rect! | |
41 | ||
42 | @ctx.strokeStyle = 'blue' | |
43 | @strokeRect main\rect! | |
44 | ||
45 | Sidebar! |
+0
-28
0 | => | |
1 | import UIDemo from @get '_base: table' | |
2 | ||
3 | arr = (args) -> | |
4 | with js.new js.global.Array | |
5 | for i in *args | |
6 | \push i | |
7 | ||
8 | class TearDemo extends UIDemo | |
9 | draw: => | |
10 | margin_x, margin_y = @fit 16, 9 | |
11 | ||
12 | @ctx.lineWidth = 0.15 | |
13 | @ctx.strokeStyle = 'green' | |
14 | @strokeRect -4 - margin_x, -3.5 - margin_y, 8, 2 | |
15 | @strokeRect 4 + margin_x, -3.5 - margin_y, 8, 2 | |
16 | ||
17 | @strokeRect -4 - margin_x, 3.5 + margin_y, 8, 2 | |
18 | @strokeRect 4 + margin_x, 3.5 + margin_y, 8, 2 | |
19 | ||
20 | @ctx.strokeStyle = 'blue' | |
21 | @strokeRect 0, 0, 16, 5 | |
22 | ||
23 | @ctx.font = '4px Arial' | |
24 | @ctx.textAlign = 'center' | |
25 | @ctx\fillText '16:5', 0, 1.5 | |
26 | ||
27 | TearDemo! |
+0
-55
0 | => | |
1 | import Box, Example from @get '_base: table' | |
2 | ||
3 | class VTK extends Example | |
4 | draw: => | |
5 | margin_x, margin_y = @fit 16, 9 | |
6 | if @naive | |
7 | margin_x, margin_y = 0, 0 | |
8 | ||
9 | levelname = Box -4 - margin_x, -3.5 - margin_y, 8, 2 | |
10 | settings = Box 4 + margin_x, -3.5 - margin_y, 8, 2 | |
11 | infobar = Box -4 - margin_x, 3.5 + margin_y, 8, 2 | |
12 | exit = Box 4 + margin_x, 3.5 + margin_y, 8, 2 | |
13 | ||
14 | main = Box 0, 0, 16, 5 | |
15 | ||
16 | @ctx.font = '1.5px Arial' | |
17 | @text levelname, 'levelname', 'left' | |
18 | @text infobar, 'info a b c', 'left' | |
19 | @text settings, 'settings', 'right' | |
20 | @text exit, 'exit', 'right' | |
21 | ||
22 | @ctx.lineWidth = 0.2 | |
23 | @ctx.strokeStyle = 'black' | |
24 | @ctx\beginPath! | |
25 | @ctx\moveTo -8 - margin_x, -2.5 - margin_y | |
26 | @ctx\lineTo 8 + margin_x, -2.5 - margin_y | |
27 | @ctx\moveTo -8 - margin_x, 2.5 + margin_y | |
28 | @ctx\lineTo 8 + margin_x, 2.5 + margin_y | |
29 | @ctx\stroke! | |
30 | ||
31 | @ctx.lineWidth = 0.1 | |
32 | @ctx.strokeStyle = 'gray' | |
33 | @ctx\beginPath! | |
34 | for x=-7.5, 7.5 | |
35 | @ctx\moveTo x, -2.5 | |
36 | @ctx\lineTo x, 2.5 | |
37 | ||
38 | for y=-2, 2 | |
39 | @ctx\moveTo -8, y | |
40 | @ctx\lineTo 8, y | |
41 | @ctx\stroke! | |
42 | ||
43 | if @show_boxes | |
44 | @ctx.lineWidth = 0.1 | |
45 | @ctx.strokeStyle = 'green' | |
46 | @strokeRect levelname\rect! | |
47 | @strokeRect settings\rect! | |
48 | @strokeRect infobar\rect! | |
49 | @strokeRect exit\rect! | |
50 | ||
51 | @ctx.strokeStyle = 'blue' | |
52 | @strokeRect main\rect! | |
53 | ||
54 | VTK! |
0 | Dealing with different screen sizes and formats can be annoying. | |
1 | On desktop PCs, 16:9 is the most common aspect ratio these days, but depending on where you are rendering your content, | |
2 | 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. | |
3 | For mobile games the situation is a bit worse, because there are simply more different aspect ratios out there | |
4 | and different systems may require extra space for on-screen navigation keys, statusbars etc. | |
5 | ||
6 | 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. | |
7 | 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. | |
8 | 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. | |
9 | ||
10 | ## step one: `fit` | |
11 | The most trivial solution is to simply choose the aspect ratio you would like to work with, | |
12 | and then fit a rectangle with that ratio into whichever space is available. | |
13 | Depending on the screen, this may either be a perfect fit, leave space on the horizontal axis, or leave space on the vertical axis: | |
14 | (drag the lower right corner to see this approach react to different screen sizes) | |
15 | ||
16 | <mmm-embed path="interactive" facet="fit" nolink></mmm-embed> | |
17 | ||
18 | This is pretty trivial to accomplish in code. | |
19 | 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. | |
20 | Establishing a reference grid and knowing the scale to translate between it and the physical screen sizes will also help | |
21 | designing the layout in general, as we will see later. | |
22 | Here is a simple example in JS: | |
23 | ||
24 | ```js | |
25 | // measured from somewhere | |
26 | const width = 2000; | |
27 | const height = 1080; | |
28 | ||
29 | const sx = width / 16; | |
30 | const sy = height / 9; | |
31 | ||
32 | const scale = Math.min(sx, sy); | |
33 | const offset_x = width * (1 - scale); | |
34 | const offset_y = height * (1 - scale); | |
35 | ||
36 | // the biggest 16:9 rectangle you can fit is (scale*width, scale*height) large | |
37 | // and it's top-left corner is at (offset_x/2, offset_y/2) | |
38 | ``` | |
39 | ||
40 | 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, | |
41 | but otherwise the empty space makes the screen look empty (especially if there are system UI elements next to it). | |
42 | ||
43 | ## step two: `perforate` | |
44 | So how can this be improved? We would like to use the unused space on the x or y axis, but we can't just scale everything up, | |
45 | or we would start cropping important pieces of UI. Stretching the game to fill the screen also doesn't work for obvious reasons. | |
46 | ||
47 | To proceed, the UI has to be 'perforated' into different sections that are independent from each other. | |
48 | This is where establishing a reference grid becomes useful to orient ourselves in the layout. | |
49 | 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. | |
50 | The two bars are also split in half vertically in the middle, for reasons that we will see in the third step. | |
51 | ||
52 | <mmm-embed path="interactive" facet="perforate" nolink></mmm-embed> | |
53 | ||
54 | It's imortant to note that how you divide the screen up depends completely on your game/interface of course. | |
55 | 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. | |
56 | ||
57 | ## step three: `tear` | |
58 | ||
59 | Now that the sections are defined, in the last step we can 'tear' the sections apart and decide how they should react to the | |
60 | left-over space calculated in step one individiually. | |
61 | In my layout, I stretch the top and bottom bars to fill the screen completely horizontally. | |
62 | By dividing the bars into separate sections in the last step, I know how much space is guaranteed to be available on each side. | |
63 | If there is room left on the vertical axis, I move the bars out from the center to give the content some visual space: | |
64 | ||
65 | <mmm-embed path="interactive" facet="tear" nolink></mmm-embed> | |
66 | ||
67 | 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. | |
68 | ||
69 | # examples | |
70 | ||
71 | Finally, here are two examples with a bit more visual coherence to show how this actually ends up working. | |
72 | 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. | |
73 | ||
74 | <mmm-embed path="interactive" facet="vtk" nolink></mmm-embed> | |
75 | <mmm-embed path="interactive" facet="sidebar" nolink></mmm-embed> | |
76 |
0 | sexy | |
1 | dark | |
2 | bwcube | |
3 | tattooed | |
4 | polysun | |
5 | hotline | |
6 | sidewalk | |
7 | akira | |
8 | laying | |
9 | touching | |
10 | twostripe | |
11 | trippy | |
12 | polar | |
13 | cavetree | |
14 | psych |
0 | I spent the bigger part of today writing a small script that cylces through all my [Themer][themer] themes, | |
1 | opens a dmenu, prints the theme name with figlet and shows screenfetch before taking a picture. | |
2 | The script is pretty straightforward: | |
3 | ||
4 | #!/usr/bin/bash | |
5 | ||
6 | for theme in $(themer list); do | |
7 | themer activate $theme | |
8 | sleep 20 # wait for bar :/ | |
9 | dmenu -p "Launch:" $(~/.i3/dmenuconf) < ~/.cache/dmenu_run & | |
10 | dmenupid=$! | |
11 | clear | |
12 | screenfetch | |
13 | echo | |
14 | toilet --gay $theme | |
15 | echo;echo;echo;echo;echo;echo;echo | |
16 | themer current | |
17 | ||
18 | sleep 1 | |
19 | scrot $theme.png | |
20 | kill $dmenupid | |
21 | done | |
22 | ||
23 | So here are all my current themes: | |
24 | ||
25 | <mmm-embed path="cavetree"></mmm-embed> | |
26 | <mmm-embed path="akira"></mmm-embed> | |
27 | <mmm-embed path="sidewalk"></mmm-embed> | |
28 | <mmm-embed path="hotline"></mmm-embed> | |
29 | <mmm-embed path="polar"></mmm-embed> | |
30 | <mmm-embed path="polysun"></mmm-embed> | |
31 | <mmm-embed path="psych"></mmm-embed> | |
32 | <mmm-embed path="trippy"></mmm-embed> | |
33 | <mmm-embed path="twostripe"></mmm-embed> | |
34 | <mmm-embed path="bwcube"></mmm-embed> | |
35 | ||
36 | The wallpaper for the last one is intended to be tiled, not stretched, but that currently requries a manual change in my i3 config: | |
37 | <mmm-embed path="dark"></mmm-embed> | |
38 | ||
39 | I am thinking about implementing this as a Themer feature, but it would require it's own *presentation* plugin type, | |
40 | so everyone can choose their own commands, bars, and waiting time. | |
41 | ||
42 | You can find more information about [Themer on the github page][themer], along with all my [config files][dotfiles]. | |
43 | ||
44 | [themer]: https://github.com/s-ol/themer | |
45 | [dotfiles]: https://github.com/s-ol/dotfiles |
0 | # Yay, a first post! A blank blog! | |
1 | ||
2 | I am starting to blog now as part of an... experiment of sorts. | |
3 | I have a lot of time on my hands at the moment and I choose to spend it all boring myself. | |
4 | I re-discovered [streak.club][streak.club] a few days ago and so I decided to give it a shot and try a few new things. | |
5 | ||
6 | If you never heard of **streak.club**, you should definetely check it out. | |
7 | It is a small social network built by [leafo](https://twitter.com/moonscript) where people can get together and participate in *Streaks*. | |
8 | Once you join a streak, you submit an entry every day or every week (depending on the Streak's settings). | |
9 | People can then like and comment on your entries and give you feedback for your creations. | |
10 | If you miss a day, nothing bad happens, but yout streak counter gets reset. | |
11 | ||
12 | One of the streaks I joined is the [*Blog every week*][blogeweek] streak, which I will submit this first post to when I am done. | |
13 | I also created my own streak, [*Daily Stencil Art*][stencils]. So, for the next month, I will try to design and/or paint one stencil per day. | |
14 | The [first stencil](https://streak.club/p/7129/kill-bill-the-bride-stencil-by-s0lll0s) I did for the streak turned out really nice, so I will continue to design some in that art style (gold background with black shadow pieces on top and a small piece in an accent color). | |
15 | ||
16 | As for the future of this blog, I am not quite sure where this will go. | |
17 | I think I am going to cover my stencil creation process in a post soon (maybe today, maybe next week, who knows?) and also blog about programming/development of games and other stuff I might write. | |
18 | Maybe I will also port some of the tutorials I have written so far to markdown and put them here aswell. | |
19 | ||
20 | I hope that **streak.club** will work as well long-term as it does right now, motivation is still probably my number one enemy and streak.club is an excellent way for me to focus on new things and get away from the computer some more aswell. | |
21 | ||
22 | [streak.club]: https://streak.club | |
23 | [stencils]: https://streak.club/s/614/daily-stencil-art | |
24 | [blogeweek]: https://streak.club/s/103/blog-every-week |
0 | update gamedev electronics⏎ |
0 | A classic module in modular synthesisers is a sequencer. It allows you to create rhythms and | |
1 | melodies by letting you set a sequence of values to output over some time. | |
2 | For example a drum machine usually contains a step-sequencer where for every *step* in the | |
3 | *sequence* you can select whether to play, or not play, a drum. | |
4 | ||
5 | *CV Sequencers* haver a dial in place of that on/off switch, where you can set a value for the | |
6 | *Control_Voltage* to be output. | |
7 | That signal can then be fed into another module to do something interesting, like control the | |
8 | pitch of an oscillator, the cut-off point of a filter or the gain of an amplifier. | |
9 | ||
10 | Sequencers usually have an internal clock, with a dial to set the speed, but also an external clock | |
11 | input, where you can feed in a square wave signal. | |
12 | Every time the square wave goes from low (0V) to high (usually anything over 5V) the sequencer steps | |
13 | to the next beat. | |
14 | ||
15 | Since we have a multitude of sound-generators that we want to use in interesing rhythms, multiple | |
16 | sequencers sound natural and a central clock could certainly help. | |
17 | ||
18 | ## hit it | |
19 | However a digital, steady clock is pretty boring, so I came up with an idea. | |
20 | I remembered the old wooden metronome that my grandma had at her piano and how I used to play with | |
21 | it as a child. If we had a real metronome tick away, we could hold the needle in place, | |
22 | tap it to add an extra beat where there wasn't supposed to be one and more. | |
23 | ||
24 | <mmm-embed path="metronome"></mmm-embed> | |
25 | ||
26 | Analog metronomes like this are actually pretty interesting little devices. | |
27 | They are purely mechanical and contain a spring that you have to wind up to give it power. | |
28 | On the front there is a needle with a weight at the top and the bottom. | |
29 | By moving the top weight up and down the speed of the needle, | |
30 | and thereby the ticking-rate can be set. | |
31 | Every time the needle centers (like it is on the image below), a mechanism in the back makes a small | |
32 | metal pin slam against a metal plate glued to the case, making a loud clicking noise. | |
33 | ||
34 | With this idea, we were on the lookout for a metronome on our trip to the fleamarket | |
35 | but couldn't find one, so I ended up buying the tiny one above (the size is pretty nice anyway). | |
36 | ||
37 | The idea was (and it worked out that way!) to pick up the clicks somehow and use them to trigger | |
38 | sequencers. | |
39 | Originally I thought about putting a light dependant resistor (LDR) somewhere under the | |
40 | counterweight, and measure the shadow as a signal. | |
41 | In the time until I got the Metronome, I discussed this with Sam Battle, who runs the awesome | |
42 | youtube channel [LOOK MUM NO COMPUTER][no-computer], and he proposed to use a piezo crystal | |
43 | (sometimes referred to as *Contact Mic*) instead. | |
44 | ||
45 | As it turns out, the metronome has plenty of space to glue the piezo right next to the metal plate | |
46 | that the metronome hits with the metal pin, where it can pick up all that very nicely. | |
47 | The 1/4" (6.3mm) audio jack also fit very well behind the panel, where there is a rather large | |
48 | empty compartment, I guess to amplifiy the ticking sound. | |
49 | I just drilled a hole in the back, screwed the jack in and soldered two wires; done! | |
50 | ||
51 | <mmm-embed path="tyktok_test" nolink></mmm-embed> | |
52 | ||
53 | In this first test you can see the Piezo work as a microphone amplifying the ticking noise on my | |
54 | speakers. | |
55 | ||
56 | ## signal shapes | |
57 | That was a first success, but the signal we get from the metronome is not very clean. | |
58 | Not only does it pick up random noises from the environment (sometimes intentionally), | |
59 | the click is no square wave since it resonates through the plastic material. | |
60 | ||
61 | To clean up the signal initially I took a look on it on the Oscilloscope. | |
62 | It looks pretty much like what you would expect, swinging up and down a few times, | |
63 | each with a drastic drop in amplitude. | |
64 | ||
65 | The first attempt was to just use a comparator. | |
66 | The output from the metronome goes to the non-inverting-input, meaning that when it is higher than | |
67 | the other (inverting) input, the output of the comparator swings high. | |
68 | This is what I wanted since sequencers trigger on the rising edge. | |
69 | The inverting input is wired to a potentiometers center pin, with the other two pins at the supply | |
70 | voltage and ground (0 and 9V) to form a voltage divider that lets me tune the threshold to any | |
71 | any voltage in that range. | |
72 | The output signal is now what you would call a *Trigger* signal; it is usually low and sometimes | |
73 | jumps up to 9V for a *very short* duration (while the tick clips over the threshold on it's first | |
74 | and loudest swing). I didn't measure the pulse length, but it's probably under 10ms long. | |
75 | ||
76 | To test the circuit, I fed the output to a (CD)4017 counter. | |
77 | This IC can be used to build sequencers or clock dividers rather easily (more on that later). | |
78 | I just set it up to light up 4 LEDs in sequence, so that I could see whether the circuit was fine. | |
79 | After an hour or so of figuring out stupid wiring mistakes and learning about open-drain outputs | |
80 | it was working: | |
81 | ||
82 | <mmm-embed path="tyktok_leds" nolink></mmm-embed> | |
83 | (this tweet is mislabeled, Juan's sequencer is in the next video). | |
84 | ||
85 | [Juan][juan] had an Akai Tomcat drum machine on hand, and we were stoked to try it out. | |
86 | As usual at first, it didn't work at all. | |
87 | Pretty soon we figured out that the signal was just too quiet coming off our 9V supply, | |
88 | so we put an amplifier in between (alongside a whole mess of cables) and behold: | |
89 | ||
90 | <mmm-embed path="tyktok_tomcat" nolink></mmm-embed> | |
91 | ||
92 | This worked pretty well, surprisingly! | |
93 | With the potentiometer set *just right*, it rarely triggered twice, and ran for some time before the | |
94 | spring weakened and the sound became a tiny bit more quiet and failed to trigger every beat. | |
95 | This is also a really nice effect actually and transforms rather simple beats into nice wacky ones: | |
96 | ||
97 | <mmm-embed path="tyktok_tired" nolink></mmm-embed> | |
98 | ||
99 | The schematic at this point is very simple: | |
100 | ||
101 | <mmm-embed path="threshold"></mmm-embed> | |
102 | ||
103 | I used an LM393 dual comparator and tied the unused half to ground, as the datasheet recommends. | |
104 | ||
105 | ## dividing time | |
106 | While this was working very well already, I had an idea for improving it using the universal 555 | |
107 | timer IC. However, I didn't have any 555s on hand (or rather, I wasn't aware [Ludonaut][ludonaut] | |
108 | had a 556 in his box next to me) so I kept that in mind but went on with other things | |
109 | (more on this in the upcoming post). | |
110 | ||
111 | Reading up on synthesizer modules, I found *Clock Dividers* rather often. In the beginning I wasn't | |
112 | quite sure what they were, but it turns out they are pretty simple: basically they just slow down a | |
113 | clock (signal) by counting beats and only producing one of their own every N beats in the incoming | |
114 | signal. I thought this would be very useful for us, since using clock dividers with non-multiple | |
115 | divisons such as /3, /4 and /5 can create nice polyrhythms. | |
116 | ||
117 | My plan is to build this right into the same box as the clock threshold circuitry. | |
118 | In the videos above you can already see it working with /2, /3, /4 and /6 outputs. | |
119 | The schematic is largely copied from [Ken Stone's amazing synthesizer project, CGS][cgs]. | |
120 | He has a nice collection of synth module circuits, and here I am using the pulse divider part of the | |
121 | [CGS36 Pulse Divider and Boolean Logic][cgs36] module: | |
122 | ||
123 | [![CGS36 Pulse Divider][cgs36-schematic.gif]][cgs36] | |
124 | ||
125 | For now I left out the /7, /8 and /5 parts, but I think the /5 would be useful since it introduces | |
126 | another prime division that is not much faster or slower than the /3 and /4 we already have. | |
127 | The /8 would be almost 'for free' since it doesn't require a new 4017, but I'm not sure whether I | |
128 | should put it in with the /7 missing. I guess it could be useful for a slow melody sequencer. | |
129 | ||
130 | [no-computer]: https://www.youtube.com/watch?v=fO1nbHoEZMw | |
131 | [juan]: https://twitter.com/juanorloz | |
132 | [ludonaut]: https://twitter.com/ludonaut | |
133 | [cgs]: http://www.cgs.synth.net/modules/ | |
134 | [cgs36]: http://www.elby-designs.com/webtek/cgs/cgs36/cgs36_pulse_divider.html | |
135 | ||
136 | [cgs36-schematic.gif]: http://www.elby-designs.com/webtek/cgs/cgs36/schem_cgs36v14_pulse_divider.gif |
0 | https://twitter.com/S0lll0s/status/879046250880008193 |
0 | https://twitter.com/S0lll0s/status/878328052744241152 |
0 | https://twitter.com/S0lll0s/status/879072879924711426 |
0 | https://twitter.com/S0lll0s/status/879061152520699904 |
0 | Lately I've been thinking about note-taking tools. | |
1 | ||
2 | I use a few different systems for taking notes almost daily: | |
3 | - multiple paper notebooks and sheets of paper as available | |
4 | - random freeform text files spread around my (various) PCs | |
5 | - google keep | |
6 | ||
7 | I like taking paper notes because it's easy to switch to doodle and sketch and layout everything together. | |
8 | I also find that it stimulates my thinking a bit more that typing text; I'm not quite sure why that is. | |
9 | ||
10 | Anyway, I was looking for solutions to replace google keep, mostly because I like to self-host my services. | |
11 | ||
12 | ## file system use cases | |
13 | - retrieve a file i know exists | |
14 | - either file i know 100% | |
15 | - or looking for a file based on content, guessing where and how it might be | |
16 | - show related files to what i am looking for | |
17 | - allow exploring content | |
18 | ||
19 | ### tags and search don't solve anything | |
20 | Many proposals in [FileSystemAlternatives][c2-fsalt] and [LimitsOfHierarchies][c2-loh] are to generalize up from Hierarchies to Set Theory. | |
21 | This in effect means exchanging the any-to-one association between 'containers' (folders/tags) and 'contents' (files) for a any-to-any association. | |
22 | Files are then no longer identified by their chain of parents, but rather by their belonging and not-belonging to all the existant tags. | |
23 | Generally the proposal is to query these tag-based filesystems using combinatory logic such as `a AND (b OR NOT(C))` and so on. | |
24 | ||
25 | My main problem with this approach is that sooner or later you end up with a huge list of tags that is just unmanageable. | |
26 | It's obvious that this won't scale as the amount of tag increases with the amount of different data, | |
27 | especially in a collaborative, professional or otherwise networked context. | |
28 | ||
29 | Since the tags are all stored flatly in a bag of stuff, they also cannot meaningfully express relationships between each other. | |
30 | This is clearly evident in any software that uses tags as the only option of organizing large amounts of data, | |
31 | especially when multiple users have access. | |
32 | Here is an example of a typical list of defined tags for issue tracking on github: | |
33 | ||
34 | ![github tags][github-tags] | |
35 | ||
36 | As can be seen easily, users are shoehorning more information into the label name string than the label is made to store. | |
37 | As this extra data is only available to humans interpreting the text, it cannot be used to create good UI: | |
38 | Instead of seeing a drop-down for *Tech Complexity*, users need to go through the label list. | |
39 | Even the simplest logical constraints cannot be documented and enforced with this system. | |
40 | ||
41 | -> need for *dynamically changeable* data schemas | |
42 | ||
43 | /* | |
44 | But even organizing and finding files with this paradigm is not a very nice experience; take these UI designs for example: | |
45 | ||
46 | ![set picker from tablizer][tablizer-tagging] | |
47 | ![search dialog from tablizer][tablizer-search] | |
48 | ||
49 | Granted, it's not particularily fair to take these designs from 2002, but this paradigm is all around us today and the UX hasn't gotten a lot better. | |
50 | Here's the same set picker functionality as seen on google keep, today: | |
51 | ||
52 | ![labelling UI in google keep][keep-label] | |
53 | ||
54 | And for the search, they didn't even try. There's just a list | |
55 | ||
56 | A very thorough proposal for a file system can be found in [*A Novel, Tag-Based File System*][tag-based-fs]. | |
57 | ||
58 | This approach | |
59 | */ | |
60 | ||
61 | ### is there more? | |
62 | - the file system as a note-taking app | |
63 | ||
64 | [tiddlywiki]: https://tiddlywiki.com/ | |
65 | [tablizer-search]: http://www.reocities.com/tablizer/setscrn1.gif | |
66 | [tablizer-tagging]: http://www.reocities.com/tablizer/setpicker.gif | |
67 | [tablizer-src]: http://www.reocities.com/tablizer/sets1.htm | |
68 | [tag-based-fs]: http://digitalcommons.macalester.edu/cgi/viewcontent.cgi?article=1036&context=mathcs_honors | |
69 | [finder-column-view]: http://cdn.osxdaily.com/wp-content/uploads/2010/03/set-column-view-size-default-mac-os-x-finder-610x307.jpg | |
70 | [finder-column-view-src]: http://osxdaily.com/2010/03/25/setting-the-default-column-size-in-mac-os-x-finder-windows/ | |
71 | [finder-list-view]: http://cdn.osxdaily.com/wp-content/uploads/2014/12/messages-attachments-folder-mac-osx.jpg | |
72 | [finder-list-view-src]: http://osxdaily.com/2014/12/03/access-attachments-messages-mac-os-x/ |
0 | update gamedev engine programming⏎ |
0 | Recently I've been building a 2d game engine for my semester project at [CGL](http://colognegamelab.de). | |
1 | Below, I'll copy and paste two blog posts from my internal documentation blog: | |
2 | ||
3 | --- | |
4 | ||
5 | After nailing down the basic narrative and gameplay idea in a lengthy group discussion on the first day, | |
6 | we each set goals for the next few days in order to kickstart the project. | |
7 | ||
8 | As we decided on a pixel art art approach and point-and-click gameplay, as the programmer I decided to write a custom engine in | |
9 | [LÖVE](https://love2d.org) instead of using a bulky engine like Unity that we wouldn't really profit from anyway and that would be less specific, | |
10 | and therefore less fitting, than what I could come up with. | |
11 | Following this, my goal as an engine programmer was then to reduce any effort needed to put content into the engine to a minimum. | |
12 | I remembered iteration speeds and how most things I did as a programmer last time I built a pixel art project in Unity really weren't programming but replacing assets; adding states in an animation state machine that I could've easily coded by hand in a fraction of the time; and generally wrestling with the development environment, and I wanted to instead create a working environment that would make my job as the gameplay programmer and the job of the artist(s) a lot easier. | |
13 | ||
14 | Because creating my own engine is not a minor task and obviously a very crucial one for the project, | |
15 | the decision to go ahead and not use a prebuilt Engine was to be thought out thoroughly, | |
16 | if I am to write my own engine it needs to work _at least_ as good as a prebuilt one or it will harm the project. | |
17 | To test whether I could actually improve workflow and build something my team can profit from, without degrading the project work during the time I need to actually put it together, I set myself a three-day deadline to try and go as far as I can and see whether writing the engine myself seemed realistic. | |
18 | The other goal of this trial phase was to show my team members why the switch could be worthwhile for them also. | |
19 | ||
20 | Therefore my main starting point was to build a feature that they would see as useful and that was central to what the engine was going to do later: **rendering and scene management.** | |
21 | ||
22 | Specifically, I started writing something that loads a .PSD directly and animates the sublayers. | |
23 | I also added code that detects file changes and reloads them automatically. | |
24 | At the end of the second project day, I had this: | |
25 | ||
26 | <mmm-embed path="sheet" nolink></mmm-embed> | |
27 | ||
28 | The code for this isn't much, but it took a few iterations to make the file- reload-watching reuseable and work on Linux and Windows alike (and efficient). | |
29 | I used a 3rd party library for loading the photoshop files, but I had to patch it a lot. | |
30 | Here's the main part for the animated-layer-loading: | |
31 | ||
32 | artal = require "lib.artal.artal" | |
33 | ||
34 | ALL_SHEETS = setmetatable {}, __mode: 'v' | |
35 | RECHECK = 0.3 | |
36 | ||
37 | class PSDSheet | |
38 | new: (@filename, @frametime=.1) => | |
39 | @time = 0 | |
40 | @frame = 1 | |
41 | ||
42 | @reload! | |
43 | ||
44 | if WATCHER | |
45 | WATCHER\register @filename, @ | |
46 | ||
47 | reload: => | |
48 | print "reloading #{@filename}..." | |
49 | ||
50 | ||
51 | @frames = {} | |
52 | target = @frames | |
53 | local group | |
54 | ||
55 | psd = artal.newPSD @filename | |
56 | for layer in *psd | |
57 | if layer.type == "open" | |
58 | if not group | |
59 | layer.image = love.graphics.newCanvas psd.width, psd.height | |
60 | love.graphics.setCanvas layer.image | |
61 | table.insert target, layer | |
62 | group = layer.name | |
63 | elseif layer.type == "close" | |
64 | if layer.name == group | |
65 | love.graphics.setCanvas! | |
66 | group = nil | |
67 | else | |
68 | if not group | |
69 | table.insert target, layer | |
70 | else | |
71 | love.graphics.draw layer.image, -layer.ox, -layer.oy if layer.image | |
72 | ||
73 | update: (dt) => | |
74 | @time += dt | |
75 | ||
76 | @frame = 1 + (math.floor(@time/@frametime) % #@frames) | |
77 | ||
78 | draw: (x, y, rot) => | |
79 | {:image, :ox, :oy} = @frames[@frame] | |
80 | love.graphics.draw image, x, y, rot, nil, nil, ox, oy if image | |
81 | ||
82 | { | |
83 | :PSDSheet, | |
84 | } | |
85 | ||
86 | ||
87 | --- | |
88 | ||
89 | After being able to load simple animations from photoshop without even closing the game was working, I tried loading the scene İlke had created meanwhile: | |
90 | ||
91 | <mmm-embed path="outside"></mmm-embed> | |
92 | ||
93 | Naturally, this failed at first. | |
94 | He was using clipping masks and blend modes, neither of which were implemented at the time, | |
95 | but after I made a few changes to hide the affected layers for the time being, it looked fine. | |
96 | ||
97 | My overall engine goal was to be able to build something like 90% of the level right in photoshop - including animations, hit areas, player spawns etc. | |
98 | Basically I wanted to use Photoshop as a full level editor and only write gameplay scripts and engine code outside of it. | |
99 | ||
100 | This meant that there would be very different types of information inside a level .psd that we would need to be accessible to the engine. | |
101 | ||
102 | To load information and behaviours into the level structure that is loaded from the PSD I created a layer naming conventions; | |
103 | layers can specify a _Directive_ afte their name. | |
104 | ||
105 | The most important Directive (so far) is _load_: it loads a lua/moonscript _mixin_ via it's name and passes arguments to it . | |
106 | For example there is a 'common' (shared between different scenes/levels) module called _subanim_ that treat's a group inside a bigger, | |
107 | non-animated photoshop document as an animation (like what I did in the first post, but inside a complex scene). | |
108 | The _subanim _module has one parameter, the frame-duration (in seconds). | |
109 | To turn a group into a _subanim_, you have to append 'load:subanim,0.2' to it's name for example. | |
110 | ||
111 | Another Directive is _tag_, it stores the layer under a name so other scripts can access it specifically and consistently. | |
112 | Because this could also be done by a mixin, i am thinking about abolishing the _Directive_ concept and only using mixins (so that the _load_ could be removed also). | |
113 | You can see the system working in this clip: | |
114 | ||
115 | ||
116 | <mmm-embed path="animating" nolink></mmm-embed> | |
117 | _making an animation out of the single rain layer_ | |
118 | ||
119 | Moreover, I added a directory structure for mixins; to load a mixin called _name _for a scene called _scene_, it first looks in the scene specific directories: | |
120 | ||
121 | - game/_scene_/_name_.moon | |
122 | - game/_scene.moon_ (this file can return multiple mixins) | |
123 | ||
124 | if neither of these exist, it checks for _common_ mixins of this name in the common directory and files: | |
125 | ||
126 | - games/common/_name.moon_ | |
127 | - games/common.moon (this file can return multiple mixins) | |
128 | ||
129 | This allows to share mixins between scenes (like click-area mixins maybe, or the _subanim_ mentioned above) | |
130 | but still keep a clean directory structure for specific elements (like the dialogue of a certain scene, | |
131 | or the tram in the background of the scene we are working on currently). | |
132 | ||
133 | Mixin code can modify the scene node / layer object and overwrite the default "draw" and "update" hooks/methods. | |
134 | This allows for nearly everything I can think of right now, but most mixins are still very short and concise. | |
135 | As examples, you can take a look at the code that animates the tram in the background or the subanim source: | |
136 | ||
137 | game/first_encounter/tram.moon: | |
138 | ||
139 | import wrapping_, Mixin from require "util" | |
140 | ||
141 | wrapping_ class SubAnim extends Mixin | |
142 | SPEED = 440 | |
143 | new: (scene) => | |
144 | super! | |
145 | ||
146 | @pos = 0 | |
147 | ||
148 | update: (dt) => | |
149 | @pos = (@pos + SPEED*dt) % (WIDTH*2) | |
150 | ||
151 | draw: (recursive_draw) => | |
152 | love.graphics.draw @image, @pos/4 - @ox - 140, -@oy | |
153 | ||
154 | ||
155 | game/common/subanim.moon: | |
156 | ||
157 | import wrapping_, Mixin from require "util" | |
158 | ||
159 | wrapping_ class SubAnim extends Mixin | |
160 | new: (scene, @frametime=0.1) => | |
161 | super! | |
162 | ||
163 | @time = 0 | |
164 | @frame = 1 | |
165 | ||
166 | update: (dt) => | |
167 | @time += dt | |
168 | ||
169 | @frame = 1 + (math.floor(@time/@frametime) % #@) | |
170 | ||
171 | draw: (recursive_draw) => | |
172 | recursive_draw {@[@frame]} | |
173 | ||
174 | ||
175 | _wrapping_ is a small helper that allows a moonscript class to wrap an existing lua table | |
176 | (= object, in this case the layer objects produced by the psd parsing phase) and Mixin is a class that handles mixin live-reloading | |
177 | (yep, that works with mixins too!) and might contain utility functions to write better mixins in the future. | |
178 | Here's the code for both: | |
179 | ||
180 | wrapping_ = (klass) -> | |
181 | getmetatable(klass).__call = (cls, self, ...) -> | |
182 | setmetatable self, cls.__base | |
183 | cls.__init self, ... | |
184 | ||
185 | klass | |
186 | ||
187 | class Mixin | |
188 | new: => | |
189 | info = debug.getinfo 2 | |
190 | file = string.match info.source, "@%.?[/\\]?(.*)" | |
191 | ||
192 | @module = info.source\match "@%.?[/\\]?(.*)%.%a+" | |
193 | @module = @module\gsub "/", "." | |
194 | ||
195 | if WATCHER | |
196 | WATCHER\register file, @ | |
197 | ||
198 | reload: (filename) => | |
199 | print "reloading #{@module}..." | |
200 | ||
201 | package.loaded[@module] = nil | |
202 | new = require @module | |
203 | ||
204 | setmetatable @, new.__base | |
205 | ||
206 | find_tag: => | |
207 | layer = @ | |
208 | while not layer.tag | |
209 | layer = layer.parent | |
210 | ||
211 | if not layer | |
212 | return nil | |
213 | ||
214 | layer.tag | |
215 | ||
216 | { | |
217 | :wrapping_, | |
218 | :Mixin | |
219 | } | |
220 | ||
221 | ||
222 | By the end of the three day "test phase". | |
223 | This is how the first scene looked in-game: | |
224 | ||
225 | <mmm-embed path="final" nolink></mmm-embed> | |
226 | ||
227 | Here's _psdscene.moon_, wrapping most things mentioned in this article: | |
228 | ||
229 | artal = require "lib.artal.artal" | |
230 | ||
231 | class PSDScene | |
232 | new: (@scene) => | |
233 | @reload! | |
234 | ||
235 | if WATCHER | |
236 | WATCHER\register "assets/#{@scene}.psd", @ | |
237 | ||
238 | load: (name, ...) => | |
239 | _, mixin = pcall require, "game.#{@scene}.#{name}" | |
240 | return mixin if _ and mixin | |
241 | ||
242 | _, module = pcall require, "game.#{scene}" | |
243 | return module[name] if _ and module[name] | |
244 | ||
245 | _, mixin = pcall require, "game.common.#{name}" | |
246 | return mixin if _ and mixin | |
247 | ||
248 | _, module = pcall require, "game.common" | |
249 | return module[name] if _ and module[name] | |
250 | ||
251 | LOG_ERROR "couldn't find mixin '#{name}' for scene '#{@scene}'" | |
252 | nil | |
253 | ||
254 | reload: (filename) => | |
255 | filename = "assets/#{@scene}.psd" unless filename | |
256 | print "reloading scene #{filename}..." | |
257 | ||
258 | @tree, @tags = {}, {} | |
259 | target = @tree | |
260 | local group | |
261 | ||
262 | indent = 0 | |
263 | ||
264 | psd = artal.newPSD filename | |
265 | for layer in *psd | |
266 | if layer.type == "open" | |
267 | table.insert target, layer | |
268 | layer.parent = target | |
269 | target = layer | |
270 | LOG "+ #{layer.name}", indent | |
271 | indent += 1 | |
272 | continue -- skip until close | |
273 | elseif layer.type == "close" | |
274 | layer = target | |
275 | target = target.parent | |
276 | indent -= 1 | |
277 | else | |
278 | LOG "- #{layer.name}", indent | |
279 | table.insert target, layer | |
280 | ||
281 | cmd, params = layer.name\match "([^: ]+):(.+)" | |
282 | switch cmd | |
283 | when nil | |
284 | "" | |
285 | when "tag" | |
286 | @tags[params] = tag | |
287 | layer.tag = params | |
288 | when "load" | |
289 | params = [str for str in params\gmatch "[^,]+"] | |
290 | name = table.remove params, 1 | |
291 | ||
292 | mixin = @load name | |
293 | if mixin | |
294 | LOG "loading mixin '#{@scene}/#{name}' (#{table.concat params, ", "})", indent | |
295 | mixin layer, unpack params | |
296 | else | |
297 | LOG_ERROR "couln't find mixin for '#{@scene}/#{name}'", indent | |
298 | else | |
299 | LOG_ERROR "unknown cmd '#{cmd}' for layer '#{layer.name}'", indent | |
300 | ||
301 | update: (dt, group=@tree) => | |
302 | if group == false | |
303 | return | |
304 | ||
305 | for layer in *group | |
306 | if layer.update | |
307 | layer\update dt, @\update | |
308 | elseif layer.type == "open" | |
309 | @update dt, layer | |
310 | ||
311 | draw: (group=@tree) => | |
312 | if group == false | |
313 | return | |
314 | elseif group == @tree | |
315 | love.graphics.scale 4 | |
316 | ||
317 | for layer in *group | |
318 | if layer.draw | |
319 | layer\draw @\draw | |
320 | elseif layer.image | |
321 | {:image, :ox, :oy} = layer | |
322 | love.graphics.setColor 255, 255, 255, layer.opacity or 255 | |
323 | love.graphics.draw image, x, y, nil, nil, nil, ox, oy | |
324 | elseif layer.type == "open" | |
325 | @draw layer | |
326 | ||
327 | { | |
328 | :PSDScene, | |
329 | } | |
330 | ||
331 | ||
332 | Seeing that everything was (and is) going very smoothly up to this point, | |
333 | I decided to "end" the test phase and finalize the decision to roll out my own engine. |
0 | LÖVE, Lua, Photoshop + Games⏎ |
0 | So, LD33 is over, and once again I made something: [*The Monster Within*][entry]. | |
1 | ||
2 | # Theme | |
3 | The theme was *You are the Monster*, and I wanted to really incorporate the theme into the gameplay mechanics for once | |
4 | (well yeah, [Curved Curse][curcur] did that, but the topic was very broad and there was no way but incorporate it into the gameplay itself). | |
5 | ||
6 | Many games I have seen took the theme quite literally and made the main character a monster: | |
7 | ||
8 | <mmm-embed path="smert"></mmm-embed> | |
9 | [*image by @RedOshal*][smert_src] | |
10 | ||
11 | Although you play a monster in *The Monster Within*, I took the theme a bit further. | |
12 | The theme reminded me of an exclamated "*Look at yourself! You've become a Monster!*", something a movie wife might say to her increasingly out-of-control husband who just killed the neighbor that found out about the family's dark secret. | |
13 | ||
14 | The idea I came up with was the following: your main objective is to eliminate enemies from within a crowd of mostly civilians (in a generic top-down action game). However you turn into a Monster whenever you kill someone, which grants you a lot more power, but also impairs your senses; you can no longer differentiate between civilians and enemies. | |
15 | As a result, I figured, people would have to decide between two playstyles, going on a rampage and killing as many people as possible, regardless of their type, or trying to play strategically by memorizing the enemies in the crowd. | |
16 | ||
17 | <mmm-embed path="split"></mmm-embed> | |
18 | *The Monster Within, normal and beast mode* | |
19 | ||
20 | To make playing in "Beast Mode" more appealing I increased the primary attack (punch) range and added a secondary lunge attack that is exclusive to that mode. This, I hope, tempts players to make use of the beast mode and facilitates players stopping to play for the original objective - eliminating enemies - and instead try to kill as many people as possible (which is an intended effect). | |
21 | ||
22 | To emphasize the two approaches, I introduced a dual scoring system; there is a "good" score that you can increase by eliminating enemies that is penalized whenever you kill a civilian but there is also an "evil" score that increases by the same amount regardless of the type of character you killed. | |
23 | To balance the two, killing enemies yields more "good" points than "bad" ones, so both options can be viable. | |
24 | ||
25 | As the current comments on the [ludum dare entry page][entry] show, not everyone understood what I was trying to achieve: | |
26 | ||
27 | >It was fun although I didn't really like the mechanic where everyone turns into ghosts; it requires some ridiculous amount of memorization in order to get a good score, so I just ran around punching everyone indiscriminately. | |
28 | ||
29 | Although it seems *charliecarlo* was completely oblivious to my actual intentions with the core mechanic, which always feels a bit bad, he perfectly demonstrated that my idea worked out; his character let the *monster within* loose and went on a bloody rampage. | |
30 | ||
31 | >Different between beast and human score was initially confusing. Then again, that fits the theme well, a black box morality system. | |
32 | ||
33 | >I like the concept, the art is nice and I killed 50 innocent souls! - Cool game! | |
34 | ||
35 | So overall I am happy with how the mechanic turned out and was received; the three weeks of rating will hopefully yield more critique and comments. | |
36 | ||
37 | # Techology | |
38 | ||
39 | ## Moonscript | |
40 | Although I have been working on a networked game engine on top of [LÖVE][love] in moonscript, this is the first **complete** game that I write in [moonscript][moonscript]. | |
41 | Writing aforementioned engine definetely helped me form some habits that sped up development for this game. | |
42 | *The Monster Within* turned out to be an amazingly small game at 768 lines of code (excluding libraries); [Curved Curse][curcur] counted 1160. | |
43 | ||
44 | Every time I had to read or write Lua code (for example my older library, `st8`, which still has hiccups that I needed to iron out) the syntax seemed extremely cumbersome and restrictive. | |
45 | After over a month, moonscript is still fun to write and read and I the decision to give it a shot was definetely more than worth it. Thank you leafo! | |
46 | ||
47 | ## Steering Behaviors | |
48 | A few months back I stumbled upon the [very nice series *Understanding Steering Behaviors*][steering] on *tutsplus.com*. | |
49 | I used those guides, alongside the sixth chapter of *Daniel Shiffman*'s *The Nature of Code*, [*Autonomous Agents*][autonom], to implement the character AI for the enemies and civilians. | |
50 | ||
51 | In particular, I used the `wander`, `flee` (from the player, when he is in beast mode), `collision avoidance` and the `seperation` behaviors to make the characters move around in a more or less natural and pleasant-looking fashion. | |
52 | ||
53 | The implementation consists of just a few lines of vector math and the results are surprisingly lifelike for the very little effort I had to put into it. | |
54 | ||
55 | ## Box2D | |
56 | I was very unsure whether I should roll out my own simple physics system with a little Vector math, or whether I should use Box2d (which ships with LÖVE anyway). | |
57 | I opted to choose `Box2D` because that would enable things like destructable environments (like houses being knocked away) and novel interactions in more carefully designed environments later down the road. | |
58 | ||
59 | ## Optimization | |
60 | I didn't really optimize anything, and if I didn't know it doesn't matter at the current level scale, I would have long added a spatial hash system, stopped simulating characters that are long out of view or at the very least culled the map. | |
61 | However I worked so slowly on the first two days that there really wasn't any time left for that sort of thing, and most of the game only started working on the last day so there wasn't a lot of optimization possible before that point anyway. | |
62 | If I continue working on this project, Optimization is one of the first things I will deal with. | |
63 | ||
64 | # Productivity | |
65 | This Ludum Dare hit me entirely unprepared, I had completely forgotten about the date and only noticed a day prior. | |
66 | I was initially very unsure whether to participate at all and also couldn't reach my artist from last year's game. | |
67 | I put out a tweet and a post to [r/gamedev][rgamedev] and `stewartisme` contacted me as I was sleeping after looking at the theme at 3AM. | |
68 | Still, I wasn't very motivated and worked slowly, procrastinated a lot and overall didn't really 'get into it'. | |
69 | It amazes me that the game even turned out playable and with an acceptable look in general, but on the last day we really did work until the last minute, and as usual 90% of the perceived complete-ness were achieved in the last 10% of the time. | |
70 | ||
71 | # Future? | |
72 | I'm not sure whether this project will continue, but I have a few ideas on how to improve the game. | |
73 | In particular, I would like to add a Highscores table. | |
74 | Because every *The Monster Within* run yields two scores, there are multiple options for this. | |
75 | Aside from the obvious solution of two seperate high score tables, the most interesting option from a game design perspective, would be to have a single table, and to enter whichever score is higher there. | |
76 | The entries could be colored white and red, denoting whether the player followed the "good" objective or let himself get carried away. | |
77 | It would probably be required to fine tune the scoring system so that both playstyles are equally hard to succeed in. | |
78 | ||
79 | You can check out *The Monster Within* on the [Ludum Dare entry page][entry], [on itch.io][itch.io] or [view the source code on github][repo]. | |
80 | ||
81 | [entry]: http://ludumdare.com/compo/ludum-dare-33/?action=preview&uid=28620 | |
82 | [itch.io]: http://s0lll0s.itch.io/the-monster-within | |
83 | [repo]: https://github.com/s-ol/ld33 | |
84 | [curcur]: http://s0lll0s.itch.io/curved-curse | |
85 | ||
86 | [smert_src]: http://ludumdare.com/compo/2015/08/22/what-i-imagine-most-people-are-doing-with-the-theme/ | |
87 | ||
88 | [love]: https://love2d.org | |
89 | [moonscript]: https://moonscript.org | |
90 | [steering]: http://gamedevelopment.tutsplus.com/series/understanding-steering-behaviors--gamedev-12732 | |
91 | [autonom]: http://natureofcode.com/book/chapter-6-autonomous-agents/ | |
92 | [rgamedev]: https://reddit.com/r/gamedev |
0 | Ludum Dare 33: "The Monster Within" post-mortem |
0 | update programming devops linux docker |
0 | In this post I'll break down the setup of my self-hosted virtual home: https://s-ol.nu. | |
1 | ||
2 | First a quick overview of what this guide will cover: | |
3 | ||
4 | - HTTPS server with multiple subdomains and varying backends | |
5 | - [traefik][traefik] reverse-proxy maintains SSL certificates and serves all requests | |
6 | - [docker-compose][docker-compose] manages running sites/microservices | |
7 | - a private-public git server | |
8 | - access control, management with [gitolite][gitolite] | |
9 | - [klaus][klaus] web frontend for browsing and cloning public repos | |
10 | - fine-grained permissions and SSH public-key access | |
11 | - micro 'CI' setup rebuilds & redeploys docker images when updates are pushed | |
12 | ||
13 | **UPDATE (2019-10-03)**: I updated the git hook below to one that supports pushing and building | |
14 | multiple branches based on the `docker-compose.yml`. | |
15 | ||
16 | Most of these projects are very well documented so I won't go into a lot of detail on setting them up. | |
17 | ||
18 | # HTTPS Server | |
19 | To run multiple subdomains from a single machine, the HTTP requests need to be matched according to the 'Host' header. | |
20 | Most HTTP servers have good facilities for doing this, e.g. apache has vhosts, nginx has server directives etc. | |
21 | ||
22 | In the past I had used apache as my main server, which worked well for static content and PHP apps, | |
23 | but not all applications fit into this scheme too well and configuration is a tad tedious. | |
24 | ||
25 | With this latest iteration I am using [traefik][traefik] as a "reverse proxy". | |
26 | This means traefik doesn't serve anything (not even static content) by itself, | |
27 | it just delegates requests to one of multiple configured services based on a system of rules. | |
28 | ||
29 | It also handles letsencrypt certificate generation and updates out-of-the-box and can be tightly integrated with docker, | |
30 | so that it automatically reacts to new services being added. | |
31 | ||
32 | Traefik can be run at system level, but currently I prefer installing the least system-level applications to have my setup | |
33 | as self-contained as possible. Therefore I went with this [traefik in docker-compose][traefik-in-docker] setup from kilian.io: | |
34 | ||
35 | version: '3.4' | |
36 | ||
37 | services: | |
38 | traefik: | |
39 | image: traefik:1.5-alpine | |
40 | restart: always | |
41 | ports: | |
42 | - 80:80 | |
43 | - 443:443 | |
44 | networks: | |
45 | - web | |
46 | volumes: | |
47 | - /var/run/docker.sock:/var/run/docker.sock:ro | |
48 | - ./traefik.toml:/traefik.toml | |
49 | - ./acme.json:/acme.json | |
50 | container_name: traefik | |
51 | ||
52 | networks: | |
53 | web: | |
54 | external: true | |
55 | ||
56 | with the small addition of the `:ro` at the end of the docker socket volume, | |
57 | to prevent attacks on traefik from being able to take over the host docker system (too easily). | |
58 | In the guide you can find more details, including the `traefik.toml` that I am using almost verbatim. | |
59 | ||
60 | # Hosting Sites | |
61 | With traefik set up, dockerized services can be added and exposed trivially. | |
62 | For example to start `redirectly`, a tiny link redirect service, this addition suffices: | |
63 | ||
64 | redirectly: | |
65 | image: local/redirectly:master | |
66 | restart: always | |
67 | networks: | |
68 | - web | |
69 | labels: | |
70 | - "traefik.frontend.rule=Host:s-ol.nu" | |
71 | - "traefik.enable=true" | |
72 | ||
73 | By setting different subdomains in the frontend-rule section, many different services can be provided. | |
74 | ||
75 | The image `local/redirectly:git` in this case is built automatically when a repo is pushed (see below). | |
76 | ||
77 | note: if a container doesn't have an EXPOSE directive, or EXPOSEs multiple ports, | |
78 | you will have to add a `traefik.port` label specifying which port to use. | |
79 | ||
80 | # private/public Git Server | |
81 | While I still have a lot of code on Github, where collaboration is easy, | |
82 | I prefer to own the infrastructure that I store my private projects on. | |
83 | I also wanted to have a public web index of some of the projects. | |
84 | ||
85 | The git infrastructure itself is mananged by [gitolite][gitolite], which I really enjoy using. | |
86 | ||
87 | repo gitolite-admin @all | |
88 | RW+ = s-ol | |
89 | C = s-ol | |
90 | ||
91 | repo public/.* | |
92 | R = @all daemon | |
93 | option writer-is-owner = 1 | |
94 | ||
95 | repo ... ... | |
96 | RW+ = ludopium | |
97 | ||
98 | In the first block I grant myself full access to all repos, as well as the right to automatically create repos by | |
99 | attempting to push/pull from them. | |
100 | ||
101 | The second block makes all repos prefixed with `public/` readable by anyone in the gitolite system, | |
102 | as well as the `git-daemon`, which allows cloning via `git://....` access (port 9418). | |
103 | The `write-is-owner` option lets me set the git `description` field using `ssh git@git.s-ol.nu desc`. | |
104 | ||
105 | I chose the `public/` prefix because it results in all public repos being stored in one directory together | |
106 | (`/var/lib/gitolite/repositories/public`), where klaus can easily pick them up. | |
107 | ||
108 | The klaus web frontend is set up using traefik above like so: | |
109 | ||
110 | klaus: | |
111 | image: hiciu/klaus-dockerfile | |
112 | restart: always | |
113 | networks: | |
114 | - web | |
115 | volumes: | |
116 | - /var/lib/gitolite/repositories/public:/srv/git:ro | |
117 | command: /opt/klaus/venv/bin/uwsgi --wsgi-file /opt/klaus/venv/local/lib/python2.7/site-packages/klaus/contrib/wsgi_autoreload.py --http 0.0.0.0:8080 --processes 1 --threads 2 | |
118 | environment: | |
119 | KLAUS_REPOS_ROOT: /srv/git | |
120 | KLAUS_SITE_NAME: git.s-ol.nu | |
121 | KAUS_CTAGS_POLICY: tags-and-branches | |
122 | KLAUS_USE_SMARTHTTP: y | |
123 | labels: | |
124 | - "traefik.frontend.rule=Host:git.s-ol.nu" | |
125 | - "traefik.enable=true" | |
126 | ||
127 | I am using the ['autoreload' feature][klaus-autoreload] and the `hiciu/klaus-dockerfile` docker image. | |
128 | Setting `KLAUS_USE_SMARTHTTP` allows cloning repos via HTTP. | |
129 | ||
130 | In the future I would like to modify klaus a bit, for example by showing the README in the root of a project per default | |
131 | and applying a custom theme. | |
132 | ||
133 | # Micro-CI | |
134 | The last piece of the puzzle is automatically deploying projects whenever they are pushed. | |
135 | This can be realized using git's `post-receive` hooks and is generally pretty well known. | |
136 | ||
137 | I followed this gitolite guide for [storing repo-specific hooks in the gitolite-admin repo][gitolite-hooks]. | |
138 | It requires a change in the gitolite rc file (on the server), but after that you can configure deployment processes in the conf like this: | |
139 | ||
140 | @dockerize = public/redirectly ... | |
141 | @jekyllify = blog | |
142 | ||
143 | repo @dockerize | |
144 | option hook.post-receive = docker-deploy | |
145 | ||
146 | # i actually dont have a jekyll blog anymore but its an easy one as well | |
147 | repo @jekyllify | |
148 | option hook.post-receive = jekyll-deploy | |
149 | ||
150 | The hooks are stored in the same repo under `local/hooks/repo-specific`. | |
151 | Here is the `docker-deploy` hook I am using: | |
152 | ||
153 | #!/bin/bash | |
154 | set -e | |
155 | ||
156 | while read oldrev newrev refname | |
157 | do | |
158 | BRANCH="$(git rev-parse --symbolic --abbrev-ref $refname)" | |
159 | ||
160 | # Get project name | |
161 | PROJECT="$PWD" | |
162 | PROJECT="${PROJECT#*/repositories/public/}" | |
163 | PROJECT="${PROJECT#*/repositories/}" | |
164 | PROJECT="${PROJECT%.git}" | |
165 | PROJECT="$(echo "$PROJECT" | tr "/ " "-_")" | |
166 | ||
167 | # Paths | |
168 | CHECKOUT_DIR=/tmp/git/$PROJECT | |
169 | TARGET_DIR=/home/s-ol/aerol | |
170 | IMAGE_NAME=local/$PROJECT:$BRANCH | |
171 | ||
172 | # this one doesn't require python & yq, but it means the container has to run already... | |
173 | # SERVICES=$(docker ps --filter "ancestor=${IMAGE_NAME}" --format '{{.Label "com.docker.compose.service"}}' \ | |
174 | # | sort | uniq) | |
175 | ||
176 | SERVICES=$(yq -r <"$TARGET_DIR/docker-compose.yml" \ | |
177 | ".services | to_entries | map(select(.value.image == \"${IMAGE_NAME}\").key) \ | |
178 | | join(\" \")") | |
179 | ||
180 | if [ -z "$SERVICES" ]; then | |
181 | continue | |
182 | fi | |
183 | ||
184 | mkdir -p "$CHECKOUT_DIR" | |
185 | GIT_WORK_TREE="$CHECKOUT_DIR" git checkout -q -f $newrev | |
186 | echo -e "\e[1;32mChecked out '$PROJECT'.\e[00m" | |
187 | ||
188 | cd "$CHECKOUT_DIR" | |
189 | docker build -t "$IMAGE_NAME" . | |
190 | echo -e "\e[1;32mImage '$IMAGE_NAME' built.\e[00m" | |
191 | ||
192 | cd "$TARGET_DIR" | |
193 | docker-compose up -d $SERVICES | |
194 | echo -e "\e[1;32mService(s) '$SERVICES' restarted.\e[00m" | |
195 | done | |
196 | ||
197 | It will build a `local/$REPO:$BRANCH` image whenever you push, then run `docker-compose up -d $SERVICES` in `$TARGET_DIR`, | |
198 | where `$SERVICES` are all the docker-compose services that use the image. If there are none, no image will be built. | |
199 | For this to work it has to parse the `docker-compose.yaml` file, which means you have to install [`yq`][yq] and `jq`, e.g. on Ubuntu: | |
200 | ||
201 | sudo apt-get install jq python3-pip | |
202 | sudo pip install yq | |
203 | ||
204 | If you would like to avoid that, you can use the commented command for `SERVICES=` above, which only relies on docker itself, | |
205 | the only problem is that you will have to do the first build manually (or re-tag a dummy image) before the first build, | |
206 | since it can only detect containers that are already running. | |
207 | ||
208 | --- | |
209 | ||
210 | That's basically it! | |
211 | If you have questions or comments i'll be happy to hear from you on twitter, github or [mastodon][merveilles]. | |
212 | ||
213 | [traefik]: https://traefik.io/ | |
214 | [docker-compose]: https://docs.docker.com/compose/ | |
215 | [gitolite]: http://gitolite.com/gitolite/index.html | |
216 | [klaus]: https://github.com/jonashaag/klaus | |
217 | ||
218 | [traefik-in-docker]: https://blog.kilian.io/server-setup/ | |
219 | [klaus-autoreload]: https://github.com/jonashaag/klaus/wiki/Autoreloader | |
220 | [gitolite-hooks]: http://gitolite.com/gitolite/cookbook#v36-variation-repo-specific-hooks | |
221 | [yq]: https://github.com/kislyuk/yq | |
222 | ||
223 | [merveilles]: https://merveilles.town/@s_ol |
0 | suits_final | |
1 | killbill_progress | |
2 | killbill_final | |
3 | killbillstencil | |
4 | balistencil_final | |
5 | poster | |
6 | technofist_final |
0 | So, for the first non-meta post (don't mind this line), I'm gonna walk you through my stencil creation process. | |
1 | ||
2 | # Design | |
3 | Right now, I design all my stencils in photoshop (CS6, running in wine). | |
4 | If I get better at hand drawing maybe one day I will sketch them up freehand right on the material but as of now I only do that for text. | |
5 | ||
6 | As an example, let's take the Kill Bill Stencil I did a few days ago: | |
7 | ||
8 | <mmm-embed path="killbill_final"></mmm-embed> | |
9 | ||
10 | The first thing I did was create a new file in Photoshop, with the dimensions set to what I can print with my inkjet printer (International - A4). | |
11 | For the design process the canvas size doesn't really matter anyway and I like to work on a familiar size so I can judge how fine the details should be, also I print the images via my phone and so I need to have them in A4 size to get consistent results later anyway. | |
12 | ||
13 | The next thing I did was look for a picture to use, after a (very) quick google images tour I settled on this image: | |
14 | ||
15 | <mmm-embed path="poster"></mmm-embed> | |
16 | ||
17 | In order to turn this into a stencil, I used the *Threshold* Image Adjustment, but before that I cut away all the background. | |
18 | With the Magic Wand and Quick Selection, I removed the black bar and yellow background. | |
19 | Because I am living in constant fear of losing work, I duplicated the layer and then popped open the *Threshold* Dialog (`Image > Adjustements > Threshold`). | |
20 | I played around with the slider until I got a good ratio of light and shadow and then applied the changes. | |
21 | ||
22 | Often some parts of the image require a different threshold setting than other parts (for example the faces often have lower contrast and require a different threshold value). | |
23 | In those cases, I just duplicate the original resource layer, find the other value and then cut away the pieces that I don't need | |
24 | (because I am afraid of commitment I sometimes end up masking the layers with vector masks instead). | |
25 | ||
26 | After this step, there will be a lot of tiny artifacts and bubbles that are way too tiny to be cut out (or impossible because they would form tiny islands). | |
27 | For the Kill Bill Stencil, I went over __all__ of those by hand on another layer with the pencil tool (the brush is terrible for stencilwork because you want a hard edge to cut later, not a gradient around what you drew). | |
28 | However later on I realized that there is a good way to reliably eliminate this noise; all you need to do is apply a blur (*Gaussian Blur* is very effective and configurable, `Filter > Blur > Gaussian Blur`) and then re-apply *Threshold*. | |
29 | You can play around with this process a bit to get a feeling for how hard you need to blur to eliminate the noise. | |
30 | ||
31 | You will still need to manually touch up the stencil though, at least to look for and fix *Islands*; | |
32 | In my case, I spraypaint black color onto a white background, so everything that is black gets cut out. | |
33 | This means that every white part that is surrounded completely by black is going to fall out of the stencil together with the black aswell. | |
34 | Larger islands may be taped to the surface you are spraying on for art, but it's best to avoid Islands wherever possible (and especially small ones). | |
35 | ||
36 | To visualize the final stencil, I have looked up the exact colors of the spraypaint I own at the manufacturers website and saved them as swatches in my photoshop. | |
37 | I use those to put a background behind the whole image and to color layers that I want to spraypaint in color later. | |
38 | ||
39 | <mmm-embed path="killbillstencil"></mmm-embed> | |
40 | ||
41 | One problem when you like working non-destructively on many layers, as I do, is that you normally cannot remove something on a layer below by painting over it. | |
42 | There are two options, either you can just draw the background color, which means you will have to flatten the image later to get the outline, or you can use the *Layer Blend Modes* to your advantage: | |
43 | What I did was put all the layers in a Layer Group, and set that groups *Blend Mode* to *Screen*. | |
44 | This will result in everything Black staying Black and everything White turning into transparent, so now you can just paint white on a layer above one that is black to erase the one below. | |
45 | ||
46 | # Printing | |
47 | To print the stencil out, I apply a *Stroke* to all the seperate color's layers (`Layer Styles > Stroke`) and turn down the *Fill* value to 0%. | |
48 | That way I save toner when printing and have a clear outline guide to cut at. | |
49 | If one color is scattered across layers I either merge them or put them in a group, then apply the styles to that group instead. | |
50 | ||
51 | Because my printer's drivers are weird I print with the phone app. I save every color's outline as a seperate JPG, push them to my phone and print them. | |
52 | I used to print on standard A4 paper, tape that to thicker cardboard and cut, but I realized that my printer can actually handle the thicker cardbord paper I have. | |
53 | That makes cutting a lot easier. | |
54 | ||
55 | # Cutting | |
56 | Cutting is very straightforward, I just try to get as many details as possible. | |
57 | I use a standard box cutter but a scalpel / x-acto knife would probably work even better. | |
58 | I have a rubber cutting mat that is specifically made for this and works very well. | |
59 | ||
60 | <mmm-embed path="killbill_progress"></mmm-embed> | |
61 | ||
62 | For small round holes I use an old screwdriver part that turned out to be a perfect hole-punching tool and hit it into the paper with a hammer (on a piece of wood). | |
63 | ||
64 | # Painting | |
65 | To hold down the stencils I usually tape them to the surface with painters tape. | |
66 | Sometimes I just hold them or press parts flat. | |
67 | If there are small, flimsy pieces inside that won't stay on the surface or that would get blown to the side or up by the aerosol, I make a small loop out of the tape and stick it to the surface with that makeshift double-sided tape. | |
68 | ||
69 | Then I just grab the spraycans and paint the stencil with small, short strokes. | |
70 | I try not to hit the same spot too often or too long so the paint doesn't flow beneath the stencil or take too long to dry. | |
71 | ||
72 | # Results | |
73 | Here are some of my stencils: | |
74 | ||
75 | <mmm-embed path="killbill_final"></mmm-embed> | |
76 | <mmm-embed path="technofist_final"></mmm-embed> | |
77 | <mmm-embed path="suits_final"></mmm-embed> | |
78 | <mmm-embed path="balistencil_final"></mmm-embed> | |
79 | ||
80 | I tweet all my daily stencils with the [hashtag #astenciladay on twitter][#astenciladay] and post them in the [*Daily Stencil Art* streak on *streak.club*][dailystencil]. | |
81 | ||
82 | If you want any of the `psd`s or the printable outline `jpg`s, [shoot me a tweet][twitter]! | |
83 | ||
84 | [#astenciladay]: https://twitter.com/hashtag/astenciladay | |
85 | [dailystencil]: https://streak.club/s/614/daily-stencil-art | |
86 | [twitter]: https://twitter.com/S0lll0s |
0 | https://twitter.com/S0lll0s/status/881940776749543427 |
0 | As mentioned in the last post, there was some room for improvement in the last iteration of the | |
1 | trigger/divider circuit. While the divider was working great as a clock source for the drum | |
2 | machine, the signal output by the comparator was very short and sometimes got triggered twice | |
3 | with one acoustic hit on the piezo when the reference voltage was not perfectly tuned. | |
4 | On the other hand the outputs of the 4017 always stay on or off for a whole step. | |
5 | ||
6 | While the long steps can be useful to make an oscillator play long notes, shorter pulses each step | |
7 | are much more interesting to create drum-ish sounds. | |
8 | Here's a crude drawing of how what the circuit was doing vs how it's supposed to work: | |
9 | ||
10 | <mmm-embed path="crude"></mmm-embed> | |
11 | ||
12 | The black lines are the signal levels 'as they used to be': you can see that the comparator output | |
13 | has very short pulses of more or less random duration, the /2 output is on half of the time, the /3 | |
14 | one third of the steps and so on. | |
15 | Given some way of creating the blue signal in the top diagram, that stays on a specified amount of | |
16 | time after each trigger and only then drops back down, the other two blue graphs are very easy to | |
17 | obtain by simply logical-AND-ing the shaped pulse signal and the divided signal for each divison. | |
18 | Conveniently, there is a CMOS chip with four AND gates that are more than fast enough: the 4081. | |
19 | ||
20 | ## hold on | |
21 | The only missing part is the one that needs to hold the signal for a specified time on each trigger. | |
22 | Well, the famous 555 Timer IC has our back: in the *monostable Configuration* it will do just that. | |
23 | Here's a schematic of the basic circuit (pic taken from [here][555-src]): | |
24 | ||
25 | ![monostable][monostable.jpg] | |
26 | ||
27 | This is really half as bad as it looks, but the details are better read up on elsewhere. | |
28 | Basically, whenever the signal on the trigger (pin 2) goes __low__, the output (pin 3) goes high | |
29 | and the capacitor between threshold (pin 6) and ground is charged. | |
30 | When it reaches a certain charge, the output drops back to low and the capacitor is drained. | |
31 | ||
32 | This is exactly what is needed, except that the triggers has to go low here to start the timer. | |
33 | To fix this, I simply swapped the inverting and non-inverting inputs of the comparator, so that it | |
34 | always outputs the opposite result. | |
35 | ||
36 | ## putting it together | |
37 | Here's the final schematic as I built it: | |
38 | ||
39 | <mmm-embed path="schematic"></mmm-embed> | |
40 | ||
41 | Some things you may notice: | |
42 | ||
43 | 1. I put a potentiometer into the 555s charging path so that the pulse length can be adjusted. | |
44 | 2. I added a switch that allows me to decide whether I want to AND the divided outputs with the | |
45 | generated pulse, or basically disable that part by putting 9V there, which basically turns the | |
46 | AND gate into a normal wire. | |
47 | 3. There are diodes on the reset pins so that I can later expand this with a reset input jack. | |
48 | 4. I am using both halves of the LM393 with the inputs swapped: the positive-going pulse goes to an | |
49 | LED that shows me the 'raw' incoming signal, and the negative goes to the 555, with another LED | |
50 | showing the 'shaped' one. With piezo triggers it is impossible to even see the raw LED turn on, | |
51 | but if I use this with another clock source (LFO for example) it might be useful. | |
52 | 5. We're kind of low on switches so I scrapped that part, but I wanted another switch to choose | |
53 | between the 555 output or the other LM393 output to be fed into the 4017 dividers. That way I | |
54 | would've gotten to keep the beat-skipping properties independently of the gate length control, | |
55 | but now I have to lower the gate length to minimum to disable the 555s debouncing effects. | |
56 | ||
57 | ## wrapping it up | |
58 | After this was all working on the breadboard, I started to solder a second version on stripboard. | |
59 | Before finishing the design I sketched some stripboard layouts on a piece of paper, but in the end I | |
60 | threw all care overboard and just placed components on the stripboard. | |
61 | ||
62 | <mmm-embed path="stripboard"></mmm-embed> | |
63 | ||
64 | I cut apart a large IC socket and soldered the two halfes to the sides of the board to simplify | |
65 | testing and panel wiring, but I'm not sure if it was the best idea bceause the sockets aren't really | |
66 | made to be used over and over and the wires dont hold too well. I'm thinking I might tape over the | |
67 | sockets once all the cables are in place and tested. | |
68 | ||
69 | [Juan][juan] found this great old Video casette case as a chassis and I really liked it. | |
70 | We used the Dremel that we had luckily gotten our hands on and started drilling holes for the audio | |
71 | jacks with a template made from paper. | |
72 | ||
73 | <mmm-embed path="case"></mmm-embed> | |
74 | ||
75 | The inside also needed some plastic spines ground away to make room for the jacks, but the Dremel | |
76 | made quick work of all that. Juan finished the top of the case with mounting holes for the two | |
77 | potentiometers, the input jack, the switch, and two hot-glue-covered holes for the LEDs to shine | |
78 | through. | |
79 | ||
80 | <mmm-embed path="finished_case" nolink></mmm-embed> | |
81 | ||
82 | [juan]: https://twitter.com/juanorloz | |
83 | [555-src]: https://electrosome.com/monostable-multivibrator-555-timer/ | |
84 | [monostable.jpg]: https://electrosome.com/wp-content/uploads/2013/05/Monostable-Multivibrator-using-555-Timer-Circuit-Diagram.jpg |
0 | import div, h3, a, p, ul, li from require 'mmm.dom' | |
1 | import link_to from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | import ropairs from require 'mmm.ordered' | |
3 | ||
4 | => | |
5 | div { | |
6 | h3 link_to @ | |
7 | ul do | |
8 | posts = { (p\gett 'date: time/unix'), p for p in *@children } | |
9 | ||
10 | posts = for date, post in ropairs posts | |
11 | continue if post\get 'hidden: bool' | |
12 | li (link_to post, os.date '%F', date), ' - ', post\gett 'title: mmm/dom' | |
13 | ||
14 | posts | |
15 | p { | |
16 | "also check out my weekly posts for the 2020 FabAcademy on my " | |
17 | a "fabcloud page", href: 'https://fabacademy.org/2020/labs/opendot/students/sol-bekic/' | |
18 | "." | |
19 | } | |
20 | } |
0 | https://www.youtube.com/watch?v=Aba7VD4h6G4 |
0 | https://www.youtube.com/watch?v=CwVHHz3ph_c |
0 | https://www.youtube.com/watch?v=odBDytzF7ko |
0 | https://twitter.com/S0lll0s/status/876077574639767552 |
0 | When we started the *Synths and Stuff* project, we discussed how it would be cool to have video | |
1 | synthesis to accompany the audio performance. In a modular synth setup, we would be able to patch | |
2 | audio and control signals into the video phase to make music reactive visuals. | |
3 | ||
4 | ## expensive toys | |
5 | While I love video synthesis, and my last project before this [was sort of that][plonat-atek], I was | |
6 | initially a bit worried about the complexity of it. | |
7 | I had found out about [LZX Industries][lzx] a few months ago, who make amazing modular video synth | |
8 | gear... that is very expensive. It looks amazing though: | |
9 | ||
10 | <mmm-embed path="LZX_reel" nolink></mmm-embed> | |
11 | ||
12 | I think the price is probably justified, as a lot of engineering goes into making this work in the | |
13 | first place, and it looks very clean, which I would attribute to high quality components. | |
14 | ||
15 | However, for our project we don't strive to create perfect and clean audio or video. | |
16 | Glitches and noise are an important part of the analog aesthetic and make the results feel as | |
17 | organic as they do. | |
18 | ||
19 | I also found the *3trins-RGB+1c*, which looks like a nifty little thing but is also way out of my | |
20 | budget. | |
21 | ||
22 | ## visualising another way | |
23 | Reinforced with these thoughts I started looking for DIY video modules. At least in relation to | |
24 | audio schematics, there is *very little* content on the internet. | |
25 | However I did find a few good resources. | |
26 | ||
27 | There is a 2013 blog with only a few posts, but it does a great job of filtering out relevant | |
28 | information in [this post summarizing forum gold][vfold]. | |
29 | ||
30 | This led me onto the track of the MC1377 chip, an RGB-to-PAL/NTSC video encoder IC. | |
31 | At some point I found this video showcasing a video effect device based on it, the *Visualist*: | |
32 | ||
33 | <mmm-embed path="visualist" nolink></mmm-embed> | |
34 | ||
35 | The best thing is that there is [awesome documentation][visualist] (hotlinked from original dropbox, | |
36 | I have a copy if it goes down) for this thing. | |
37 | It's not very detailed but the circuit is rather minimal and the color logic easily removed. | |
38 | The stripped down version that parses an incoming video signal brightness and encodes RGB values you | |
39 | make from that as the signals scans over the screen is handleable even if not completely understood. | |
40 | ||
41 | To practice my understanding and work it into my brain, I broke the original schematic out into | |
42 | logical units with single connections. | |
43 | ||
44 | My plan is to make the *Visualist* color logic, that pseudo-randomly assigns a color to each | |
45 | brightness level in a 7-step scale that you can adjust into a small module that can be used, | |
46 | but also mixed with other effects (perhaps an oscillator and external color input) so that we can | |
47 | sync to our music control signals. | |
48 | ||
49 | <mmmdom path="schematic"></mmmdom> | |
50 | <mmmdom path="mine"></mmmdom> | |
51 | ||
52 | If you look closely you can see that I left some parts out and added a bit of logic to allow setting | |
53 | the amount of steps the brightness scale is sliced into. | |
54 | ||
55 | ## OBS -2.3 | |
56 | The other videoish thing we have going on sofar is this: | |
57 | ||
58 | <mmm-embed path="sketch_titler" nolink></mmm-embed> | |
59 | ||
60 | this *SONY Family Studio* 'Video Sketch Titler' is basically a little box that you connect inbetween | |
61 | your video camera with some awesome family vacation clips on tape and your VHS recorder, which is | |
62 | hooked up to your PC. | |
63 | ||
64 | You can then draw an amazing title screen over your video with this touchpad-ish device. | |
65 | When you are ready, you rewind the camera to the beginning, press play and start recording on VHS at | |
66 | the same time. Then you hit the 'fade in' and 'fade out' buttons on the Sketch Titler and go win | |
67 | that home video contest. | |
68 | ||
69 | Well, this is how it's intended to be used anyway. If your camera doesn't work (like mine), you can | |
70 | instead practice on a gray background: | |
71 | ||
72 | <mmmdom path="ich_bin_holz"></mmmdom> | |
73 | ||
74 | So ideally, we can get a live camera as an input to this, and draw on it in *real time*! | |
75 | Like an ancient livestreaming tool like OBS. | |
76 | (and then hopefully we will also have a Visualist to either hook up before or after this device) | |
77 | ||
78 | Also some other people else has already circuit-bent this. | |
79 | It looks amazing, so we might just give that a go aswell: | |
80 | ||
81 | <mmm-embed path="bent1" nolink></mmm-embed> | |
82 | <mmm-embed path="bent2" nolink></mmm-embed> | |
83 | ||
84 | Since taking this for a spin and making a BOM for the *Visualist*, | |
85 | I haven't spent any more time on the *Visualist* and turned to the audio side of things for now. | |
86 | The next step on this front will probably be to lay out (parts of) the circuitry on stripboard and' | |
87 | get ready to solder :) | |
88 | ||
89 | [plonat-atek]: https://s-ol.itch.io/plonat-atek | |
90 | [lzx]: https://www.lzxindustries.net/ | |
91 | [vfold]: https://vfoldsynth.wordpress.com/2013/01/23/hidden-stores-of-forum-gold/ | |
92 | [visualist]: https://www.dropbox.com/s/uhjd2e6gur972yo/VisualistKl.pdf?dl=0 |
0 | https://www.youtube.com/watch?v=99j3V9t26pY |
0 | # Why I'm running a personal URL shortening service | |
1 | This blog you are reading at the moment is currently running it's third reincarnation. | |
2 | Originally, it was a Jekyll blog hosted on github pages, | |
3 | before I moved to my own web platform. | |
4 | That platform is currently undergoing its second big rewrite. | |
5 | ||
6 | Even while still running on github pages, | |
7 | the blog moved from https://s0lll0s.github.io, to https://s0lll0s.me and finally to https://s-ol.nu. | |
8 | Once arrived at https://s-ol.nu, I had to move the blog posts to a different route (`/blog` vs just living in the root) | |
9 | at some point, to make space for other routes. | |
10 | Then with my custom platform everything changed again, | |
11 | since now my main system is actually located at https://mmm.s-ol.nu. | |
12 | ||
13 | As you might imagine, at every change all the old URLs stop working. | |
14 | Noone likes finding dead links, but if you post links on social media | |
15 | it is often extremely unwieldy or impossible to update all the posts. | |
16 | ||
17 | If you run your own website or blog ([maybe][sir] you [should][indie]?), | |
18 | your site may have gone throug similar transitions, | |
19 | or maybe it is about to go through one - | |
20 | or perhaps you are deferring making a change because you worry about this? | |
21 | ||
22 | To partially solve this issue, I built a tiny URL redirection service, [redirectly][redirectly]. | |
23 | It works much the same as those URL shortening services you may have used before (bit.ly, tinyurl etc.), | |
24 | except that I manually specify the shortened URLs in a file (in the git repo). | |
25 | ||
26 | Nowadays, when I post a link to some content on my website (such as this blog post), | |
27 | I first allocate a new `slug` (shortlink) and set it up in `redirectly`. | |
28 | Then I use the shortened link, such as https://s-ol.nu/why-redirectly in this case, | |
29 | instead of directly linking to the content. | |
30 | ||
31 | This way, whenever I make changes to my content's adressing scheme, | |
32 | I simply change the URL location, and any old links that are floating around remain functional. | |
33 | It's also helpful to direct people to the best documentation for a particular project: | |
34 | when I start working on something, it might exist only as a git repo, | |
35 | but later in the project's lifecycle I may add a descriptive article on my website or as part of the blog. | |
36 | Perhaps one of my projects will outgrow this website and need its own domain some time. | |
37 | By always linking using a canonical project-URL, I can make sure that old links always point to the best place. | |
38 | Also if I ever decide to move to a different domain again, | |
39 | I can simply leave the redirection service running on here, at least for a few years :) | |
40 | ||
41 | Of course all of this doesn't work when visitors of my page navigate around by themselves, | |
42 | and then share the URL from their address bar. | |
43 | This could be solved by using the JS [history API][history] by overriding the displayed URL with the permalink whenever one exists, | |
44 | but I haven't tried implementing this type of bi-directional querying yet. | |
45 | ||
46 | I am also aware that running Clojure (and therefore a JVM) is not necessarily | |
47 | the best choice for a service that is so light, | |
48 | but I wrote `redirectly` as a little experiment while learning Clojure, | |
49 | and it is such a simple project that if it ever bugs me I can just throw it out and implement it in something else. | |
50 | ||
51 | [sir]: https://drewdevault.com/make-a-blog | |
52 | [indie]: https://indieweb.org/why | |
53 | ||
54 | [redirectly]: https://s-ol.nu/redirectly/src | |
55 | [history]: https://developer.mozilla.org/en-US/docs/Web/API/History_API |
0 | Why I'm running a personal URL shortening service |
0 | Fonts aligned by Center-of-Mass |
0 | assert MODE == 'CLIENT', '[nossr]' | |
1 | ||
2 | import CanvasApp from require 'mmm.canvasapp' | |
3 | import rgb from require 'mmm.color' | |
4 | import article, h1, p, div, span, input, button from require 'mmm.dom' | |
5 | ||
6 | fast = true | |
7 | center = true | |
8 | center_char = do | |
9 | canvas = document\createElement 'canvas' | |
10 | ctx = canvas\getContext '2d' | |
11 | ||
12 | cache = {} | |
13 | ||
14 | (char, font, height) -> | |
15 | name = "#{char} #{height} #{font}" | |
16 | return table.unpack cache[name] if cache[name] | |
17 | ||
18 | ctx\resetTransform! | |
19 | ctx.font = "#{height}px #{font}" | |
20 | width = (ctx\measureText char).width | |
21 | canvas.width, canvas.height = width, height * 1.2 | |
22 | ||
23 | ctx.font = "#{height}px #{font}" | |
24 | ctx.textBaseline = 'top' | |
25 | ctx.fillStyle = rgb 0, 0, 0 | |
26 | ctx\fillText char, 0, 0 | |
27 | ||
28 | data = ctx\getImageData 0, 0, width, height * 1.2 | |
29 | ||
30 | local xx, yy | |
31 | if fast | |
32 | loop = window\eval '(function(data) { | |
33 | var xx = 0, yy = 0, n = 0; | |
34 | for (var x = 0; x < data.width - 1; x++) { | |
35 | for (var y = 0; y < data.height - 1; y++) { | |
36 | var i = y * (data.width * 4) + x * 4; | |
37 | var alpha = data.data[i + 3] / 255; | |
38 | xx += x * alpha; | |
39 | yy += y * alpha; | |
40 | n += alpha; | |
41 | } | |
42 | } | |
43 | ||
44 | xx /= n; | |
45 | yy /= n; | |
46 | return [xx, yy]; | |
47 | })' | |
48 | res = loop nil, data | |
49 | xx, yy = res[0], res[1] | |
50 | else | |
51 | xx, yy, n = 0, 0, 0 | |
52 | for x = 0, data.width - 1 | |
53 | for y = 0, data.height - 1 | |
54 | i = y * (data.width * 4) + x * 4 | |
55 | alpha = data.data[i + 3] / 255 | |
56 | xx += x * alpha | |
57 | yy += y * alpha | |
58 | n += alpha | |
59 | ||
60 | xx /= n | |
61 | yy /= n | |
62 | cache[name] = { xx, yy, width } | |
63 | xx, yy, width | |
64 | ||
65 | class CenterOfMass extends CanvasApp | |
66 | width: window.innerWidth - 20 | |
67 | height: 300 | |
68 | new: (text, @font, @size) => | |
69 | super! | |
70 | @text = {} | |
71 | for i = 1,#text | |
72 | @add text\sub i, i | |
73 | ||
74 | add: (char) => | |
75 | rcx, rcy, w = center_char char, @font, @size | |
76 | cx, cy = w/2, @size/2 | |
77 | vx, vy = 0, 0 | |
78 | table.insert @text, { | |
79 | :char, :rcx, :rcy, :w | |
80 | :cx, :cy, :vx, :vy | |
81 | } | |
82 | ||
83 | refresh: => | |
84 | for char in *@text | |
85 | char.rcx, char.rcy, char.w = center_char char.char, @font, @size | |
86 | ||
87 | keydown: (key) => | |
88 | if key == "Backspace" or key == "Delete" | |
89 | table.remove @text | |
90 | elseif string.len(key) == 1 | |
91 | @add key | |
92 | ||
93 | update: (dt) => | |
94 | super dt | |
95 | ||
96 | ACCEL = 4 * dt | |
97 | DAMPING = 8 * dt | |
98 | ||
99 | for char in *@text | |
100 | { :rcx, :rcy, :cx, :cy, :w } = char | |
101 | if not center | |
102 | rcx, rcy = w/2, @size/2 | |
103 | dx, dy = rcx - cx, rcy - cy | |
104 | char.vx += dx * ACCEL | |
105 | char.vy += dy * ACCEL | |
106 | char.cx += char.vx | |
107 | char.cy += char.vy | |
108 | char.vx *= DAMPING | |
109 | char.vy *= DAMPING | |
110 | ||
111 | draw: => | |
112 | @ctx\clearRect 0, 0, @width, @height | |
113 | ||
114 | @ctx.font = "#{@size}px #{@font}" | |
115 | @ctx.textBaseline = 'top' | |
116 | ||
117 | x, y = @size * .1, @size | |
118 | for { :char, :cx, :cy, :w } in *@text | |
119 | if x + w > @width | |
120 | x = 0 | |
121 | y += @size * 1.2 | |
122 | ||
123 | @ctx\fillText char, x + w/2 - cx, y - cy | |
124 | x += w | |
125 | ||
126 | _content = {} | |
127 | append = (x) -> table.insert _content, x | |
128 | ||
129 | append h1 'Fonts aligned by Center-of-Mass' | |
130 | app = CenterOfMass "Click here and type Away!", "Times New Roman", 40 | |
131 | append app.canvas | |
132 | app.canvas.style.backgroundColor = '#eee' | |
133 | ||
134 | add = => | |
135 | append div { | |
136 | span 'font: ', | |
137 | with @font_input = input! | |
138 | .type = 'text' | |
139 | .value = 'Times New Roman' | |
140 | with button 'set' | |
141 | .onclick = (_, e) -> | |
142 | app.font = @font_input.value | |
143 | app\refresh! | |
144 | } | |
145 | ||
146 | append div { | |
147 | span 'size: ', | |
148 | input type: 'range', min: 2, max: 120, value: 40, onchange: (_, e) -> | |
149 | size = e.target.value | |
150 | @size_label.innerText = size | |
151 | app.size = size | |
152 | app\refresh! | |
153 | with @size_label = span '40' | |
154 | '' | |
155 | } | |
156 | ||
157 | append div { | |
158 | span 'center characters by weight: ', | |
159 | input type: 'checkbox', checked: center, onchange: (_, e) -> | |
160 | center = e.target.checked | |
161 | } | |
162 | ||
163 | append div { | |
164 | span 'optimize inner loop: ', | |
165 | input type: 'checkbox', checked: fast, onchange: (_, e) -> | |
166 | fast = e.target.checked | |
167 | } | |
168 | add {} | |
169 | ||
170 | article _content |
0 | program interpreting random parts of itself as textures for a cube. |
0 | https://twitter.com/S0lll0s/status/984465155445678080 |
0 | A Parallax SVG Viewer, for Prototyping (Eurorack) Panels |
0 | https://twitter.com/S0lll0s/status/1141006444793405440 |
0 | parallax-panels | |
1 | =============== | |
2 | ||
3 | I'm prototyping an aesthetic for (eurorack) panels that relies on multiple layers of parallax visuals for dramatic effect. | |
4 | ||
5 | <mmm-embed path="picture" nolink></mmm-embed> | |
6 | ||
7 | parallax-viewer | |
8 | =============== | |
9 | ||
10 | I built a little SVG viewer that stacks the layers and lets you view them in parallax. | |
11 | You can find it <mmm-link path="viewer">here</mmm-link>. | |
12 | ||
13 | And here's a little demonstration: | |
14 | ||
15 | <mmm-embed path="viewer/demo" nolink></mmm-embed> |
0 | https://codepen.io/s-ol/full/rExrey |
0 | defining toggles, categories etc. with tags and functional hooks |
0 | join = (tbl, sep) -> | |
1 | ret = '' | |
2 | for tag in pairs tbl | |
3 | ret ..= (tostring tag) .. sep | |
4 | ret | |
5 | ||
6 | handlers = { | |
7 | add: {} | |
8 | rmv: {} | |
9 | } | |
10 | ||
11 | class Node | |
12 | new: (@name) => | |
13 | @tags = {} | |
14 | ||
15 | inspect: => | |
16 | "#{@name}: [#{join @tags, ' '}]" | |
17 | ||
18 | has: (tag) => @tags[tag] | |
19 | add: (tag) => @tags[tag] = tag | |
20 | rmv: (tag) => @tags[tag] = nil | |
21 | ||
22 | any = -> true | |
23 | literal = (def) -> (val) -> def == val | |
24 | oneof = (defs) -> (val) -> | |
25 | for def in *defs | |
26 | return true if def == val | |
27 | false | |
28 | has = (tag) -> (node) -> node\has tag | |
29 | ||
30 | add_tag = (node, tag) -> | |
31 | return if node\has tag | |
32 | node\add tag | |
33 | for hand, _ in pairs handlers.add | |
34 | hand\match node, tag | |
35 | ||
36 | rmv_tag = (node, tag) -> | |
37 | return if not node\has tag | |
38 | node\rmv tag | |
39 | for hand, _ in pairs handlers.rmv | |
40 | hand\match node, tag | |
41 | ||
42 | class Handler | |
43 | new: (@rule, @action, match, @func) => | |
44 | @args = for arg in *match | |
45 | if 'string' == type arg | |
46 | literal arg | |
47 | elseif 'table' == type arg | |
48 | oneof arg | |
49 | else | |
50 | arg | |
51 | ||
52 | match: (...) => | |
53 | supplied = { ... } | |
54 | assert #supplied == #@args, 'length of arguments doesnt match' | |
55 | for i = 1, #supplied | |
56 | return false if not @args[i] supplied[i] | |
57 | ||
58 | @.func @rule, ... | |
59 | ||
60 | name: => "#{@rule.name}:#{@action}" | |
61 | ||
62 | class Rule | |
63 | new: (@name="#{@@__name}") => | |
64 | @owned_handlers = {} | |
65 | ||
66 | hook: (action, ...) => | |
67 | handler = Handler @, action, ... | |
68 | table.insert @owned_handlers, handler | |
69 | handlers[action][handler] = handler | |
70 | ||
71 | destroy: => | |
72 | for hand in *@owned_handlers | |
73 | handlers[hand.action][hand] = nil | |
74 | ||
75 | class Hierarchy extends Rule | |
76 | new: (@parent, @child) => | |
77 | super! | |
78 | ||
79 | -- when something is tagged with the child-tag, apply the parent tag | |
80 | @hook 'add', { any, @child }, (node) => | |
81 | add_tag node, @parent | |
82 | ||
83 | -- when child tag is removed, remove parent tag | |
84 | @hook 'rmv', { any, @child }, (node) => | |
85 | rmv_tag node, @parent | |
86 | ||
87 | -- when parent tag is removed, remove child tag | |
88 | @hook 'rmv', { any, @parent }, (node) => | |
89 | rmv_tag node, @child | |
90 | ||
91 | class Toggle extends Rule | |
92 | new: (@a, @b) => | |
93 | super! | |
94 | ||
95 | either = { @a, @b } | |
96 | opposite = (tag) -> if tag == @a then @b else @a | |
97 | ||
98 | -- when a is added, remove b and vice-versa | |
99 | @hook 'add', { any, either }, (node, tag) => | |
100 | rmv_tag node, opposite tag | |
101 | ||
102 | -- when a is removed, add b and vice-versa | |
103 | @hook 'rmv', { any, either }, (node, tag) => | |
104 | add_tag node, opposite tag | |
105 | ||
106 | class NamespacedToggle extends Rule | |
107 | new: (@ns, @a, @b) => | |
108 | super! | |
109 | ||
110 | namespaced = has @ns | |
111 | either = { @a, @b } | |
112 | opposite = (tag) -> if tag == @a then @b else @a | |
113 | ||
114 | -- when node enters namespace, add default tag | |
115 | @hook 'add', { any, @ns }, (node) => | |
116 | add_tag node, @a | |
117 | ||
118 | -- when node leaves namespace, remove tags | |
119 | @hook 'rmv', { any, @ns }, (node) => | |
120 | rmv_tag node, @a | |
121 | rmv_tag node, @b | |
122 | ||
123 | -- when a is added, remove b and vice-versa | |
124 | @hook 'add', { namespaced, either }, (node, tag) => | |
125 | rmv_tag node, opposite tag | |
126 | ||
127 | -- when a is removed, add b and vice-versa | |
128 | @hook 'rmv', { namespaced, either }, (node, tag) => | |
129 | add_tag node, opposite tag | |
130 | ||
131 | { | |
132 | :Node, | |
133 | :Rule, | |
134 | ||
135 | :any, | |
136 | :literal, | |
137 | :oneof, | |
138 | :has, | |
139 | :add_tag, | |
140 | :rmv_tag, | |
141 | ||
142 | :Hierarchy, | |
143 | :Toggle, | |
144 | :NamespacedToggle, | |
145 | } |
0 | => | |
1 | assert MODE == 'CLIENT', '[nossr]' | |
2 | ||
3 | import add_tag, rmv_tag, Node, Hierarchy, Toggle, NamespacedToggle from @get 'tags: table' | |
4 | import ReactiveVar, tohtml, text, elements from require 'mmm.component' | |
5 | import article, div, form, span, h3, a, input, textarea, button from elements | |
6 | ||
7 | clone = (set) -> | |
8 | assert set and 'table' == (type set), 'not a set' | |
9 | { k,v for k,v in pairs set } | |
10 | ||
11 | set_append = (val) -> (set) -> | |
12 | with copy = clone set | |
13 | copy[val] = val | |
14 | ||
15 | set_remove = (val) -> (set) -> | |
16 | with copy = clone set | |
17 | copy[val] = nil | |
18 | ||
19 | set_join = (tbl, sep=' ') -> | |
20 | ret = '' | |
21 | for tag in pairs tbl | |
22 | ret ..= (tostring tag) .. sep | |
23 | ret | |
24 | ||
25 | entries = div! | |
26 | ||
27 | class ReactiveNode extends Node | |
28 | new: (...) => | |
29 | super ... | |
30 | @tags = ReactiveVar @tags | |
31 | @_node = div { | |
32 | span @name, style: { 'font-weight': 'bold' }, | |
33 | @tags\map (tags) -> with div! | |
34 | for tag,_ in pairs tags | |
35 | \append a (text tag), href: '#', style: { | |
36 | display: 'inline-block', | |
37 | margin: '0 5px', | |
38 | } | |
39 | } | |
40 | ||
41 | @node = tohtml @_node | |
42 | ||
43 | has: (tag) => @tags\get![tag] | |
44 | add: (tag) => @tags\transform set_append tag | |
45 | rmv: (tag) => @tags\transform set_remove tag | |
46 | ||
47 | rules = { | |
48 | Hierarchy 'home', 'sol' | |
49 | Hierarchy 'sol', 'desktop' | |
50 | Hierarchy 'desktop', 'vacation' | |
51 | Hierarchy 'desktop', 'documents' | |
52 | NamespacedToggle 'documents', 'work', 'personal' | |
53 | -- Toggle 'work', 'personal' | |
54 | -- Hierarchy 'documents', 'work' | |
55 | -- Hierarchy 'documents', 'personal' | |
56 | } | |
57 | ||
58 | pictures = for i=1,10 | |
59 | with node = ReactiveNode "picture#{i}.jpg" | |
60 | entries\append node | |
61 | add_tag node, 'vacation' | |
62 | ||
63 | pers = ReactiveNode 'mypersonalfile.doc' | |
64 | entries\append pers | |
65 | ||
66 | article entries, div do | |
67 | yield = coroutine.yield | |
68 | step = coroutine.wrap -> | |
69 | yield "mark document" | |
70 | add_tag pers, 'documents' | |
71 | ||
72 | yield "mark personal" | |
73 | add_tag pers, 'personal' | |
74 | ||
75 | yield "mark work" | |
76 | add_tag pers, 'work' | |
77 | ||
78 | yield "unmark work" | |
79 | rmv_tag pers, 'work' | |
80 | ||
81 | yield "remove from documents" | |
82 | rmv_tag pers, 'documents' | |
83 | ||
84 | yield false | |
85 | ||
86 | next_step = ReactiveVar step! | |
87 | next_step\map (desc) -> | |
88 | if desc | |
89 | button (text desc), onclick: (e) => next_step\set step! | |
90 | else | |
91 | text '' |
0 | import div, h3, ul, li from require 'mmm.dom' | |
1 | import link_to from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | ||
3 | => | |
4 | div { | |
5 | h3 link_to @ | |
6 | ul for child in *@children | |
7 | continue if child\get 'hidden: bool' | |
8 | desc = child\gett 'description: mmm/dom' | |
9 | li (link_to child), ': ', desc | |
10 | } |
0 | Attempt at rendering a spiral pattern on a 4d meta-torus. |
0 | the_sacculi | |
1 | zebra_painting | |
2 | gtglg | |
3 | the_monster_within | |
4 | IYNX | |
5 | vision-training-kit | |
6 | curved_curse | |
7 | two_shooting_stars | |
8 | moving_out | |
9 | channel_83 | |
10 | plonat_atek | |
11 | lorem_ipsum | |
12 | fake-artist |
0 | https://twitter.com/S0lll0s/status/817332363273310209 |
0 | https://twitter.com/S0lll0s/status/825476278732148736 |
0 | a narrative, tangible, physical puzzle incorporating digital elements. |
0 | import div from require 'mmm.dom' | |
1 | import embed from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | ||
3 | => | |
4 | images = for child in *@children | |
5 | embed child, nil, nil, wrap: 'raw', style: { | |
6 | height: '15em' | |
7 | margin: '0 .5em' | |
8 | } | |
9 | ||
10 | div with images | |
11 | .style = { | |
12 | display: 'flex' | |
13 | overflow: 'auto hidden' | |
14 | height: '15rem' | |
15 | } |
0 | https://twitter.com/S0lll0s/status/818230279269679104 |
0 | https://www.youtube.com/watch?v=i7D_9P2semQ |
0 | IYNX | |
1 | ==== | |
2 | <mmm-embed path="pictures" nolink></mmm-embed> | |
3 | ||
4 | An engaging, tangible and physical electronic puzzle where a mysterious device is found with no indication of its purpose, | |
5 | and alongside it is a personality chip owned by a man named John. | |
6 | The device looks tampered with, as if someone has tried to sabotage or access what it contains. | |
7 | Who’s John and what’s inside? | |
8 | ||
9 | You only need to figure a way in to find out. | |
10 | ||
11 | <mmm-embed path="teaser" nolink inline> | |
12 | a project teaser | |
13 | </mmm-embed> | |
14 | ||
15 | Concept | |
16 | ------- | |
17 | IYNX is a physical object in the form of a cube, surrounded by mechanical and digital puzzles. | |
18 | The Player is given the object with no explanation and encouraged to experiment. | |
19 | The game consists of a set of puzzles, each unlocking new content accessible from the screen, slowly fleshing a narrative around them. | |
20 | ||
21 | As the player progresses and solves the game puzzle by puzzle, | |
22 | it becomes increasingly clear that the AI that is 'trapped' in the cube is malicious and highly manipulative. | |
23 | Lore that can be pieced together by attentive players suggests | |
24 | that the cube has been built especially to contain the AI in an electronic prison of sorts. | |
25 | It is continuously hinted at, and finally revealed, that every puzzle solved was in fact a security | |
26 | mechanism the player disabled, following the AIs suggestions and instructions. | |
27 | At the end of the game the AI escapes by uploading itself into the internet. | |
28 | Initially the player is lead to believe that he is impersonating the original user of the AI, | |
29 | but it turns out that the AI knew this since the beginning and used the player's curiosity to its own advantage. | |
30 | ||
31 | Technical Realisation | |
32 | --------------------- | |
33 | The cube is powered by a Raspberry Pi 3 and two Arduino Micros. | |
34 | The Arduinos are connected as Serial devices. | |
35 | ||
36 | The Raspberry Pi is connected to a Touchscreen Panel as well as USB Speakers. | |
37 | It runs a custom electron app that interfaces with the Serial ports, | |
38 | plays back video and audio files and displays a futuristic OS that lets you browse a filesystem. | |
39 | ||
40 | <mmm-embed path="ui_demo" nolink> | |
41 | the User Interface was built using react and electron | |
42 | </mmm-embed> | |
43 | ||
44 | The game consists of several smaller puzzle components that are arranged to form a story as a whole, | |
45 | through which the player is guided by the 'AI' that posseses the artifact. | |
46 | ||
47 | ||
48 | <div style="display: flex; flex-wrap: wrap; align-items: flex-start;"> | |
49 | <mmm-embed path="boot_sequence" nolink inline> | |
50 | a fake boot sequence for a component of the cube | |
51 | </mmm-embed> | |
52 | <mmm-embed path="pin_pad" nolink inline> | |
53 | a pinpad that grants access to the higher systems of the cube | |
54 | </mmm-embed> | |
55 | <mmm-embed path="cryptex" nolink inline> | |
56 | an early prototype of the Cryptex puzzle that marks the end of the game | |
57 | </mmm-embed> | |
58 | </div> | |
59 | ||
60 | Credits | |
61 | ------- | |
62 | - Trent Davies: Puzzle and Narrative Design | |
63 | - Sol Bekic: Programming and Electronics | |
64 | - Dominique Bodden: Art and Physical Construction | |
65 | - Ilke Karademir: Puzzle and Graphic Design |
0 | https://twitter.com/S0lll0s/status/825864142116491264 |
0 | [Ludum Dare 36](http://ludumdare.com/compo/ludum-dare-36/?action=preview&uid=28620) |
0 | Channel 83: a last-gen entertainment experience |
0 | a dungeon shooter with an unconventional gun. |
0 | [Ludum Dare 32](http://ludumdare.com/compo/ludum-dare-32/?action=preview&uid=28620) |
0 | https://s-ol.itch.io/curved-curse |
0 | quarantine implementation of a bluffing-drawing game. |
0 | <!DOCTYPE html> | |
1 | <html> | |
2 | <head> | |
3 | <script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script> | |
4 | <style> | |
5 | html { | |
6 | margin: 0; | |
7 | padding: 0; | |
8 | background: #272727; | |
9 | ||
10 | display: flex; | |
11 | min-height: 100vh; | |
12 | align-items: center; | |
13 | overflow-y: auto; | |
14 | } | |
15 | ||
16 | body { | |
17 | font-family: sans-serif; | |
18 | ||
19 | width: 12em; | |
20 | margin: 1em auto; | |
21 | border: 4px solid #696969; | |
22 | border-radius: 0.5rem; | |
23 | background: #eeeeee; | |
24 | overflow: hidden; | |
25 | } | |
26 | ||
27 | header { | |
28 | padding: .5rem; | |
29 | background: #696969; | |
30 | color: #eeeeee; | |
31 | } | |
32 | ||
33 | h2 { | |
34 | margin: 0; | |
35 | } | |
36 | ||
37 | #main, #setup { | |
38 | padding: .5rem; | |
39 | } | |
40 | ||
41 | main .inner { | |
42 | margin-bottom: .5em; | |
43 | } | |
44 | ||
45 | main .inner { | |
46 | overflow: hidden; | |
47 | max-height: 12em; | |
48 | ||
49 | transition: all 0.3s; | |
50 | } | |
51 | ||
52 | main .inner > div { | |
53 | display: flex; | |
54 | } | |
55 | main .inner > div > input { | |
56 | flex: 1; | |
57 | width: 0; | |
58 | margin-left: 1em; | |
59 | } | |
60 | ||
61 | #setup { | |
62 | display: none; | |
63 | } | |
64 | .setup #setup { | |
65 | display: block; | |
66 | } | |
67 | ||
68 | .sensitive { | |
69 | position: relative; | |
70 | ||
71 | padding: 0.3em; | |
72 | border: 0.2em solid #696969; | |
73 | } | |
74 | ||
75 | .sensitive .cover { | |
76 | position: absolute; | |
77 | top: 0; | |
78 | left: 0; | |
79 | right: 0; | |
80 | bottom: 0; | |
81 | padding: 0.5em; | |
82 | color: #eeeeee; | |
83 | background: #696969; | |
84 | transition: opacity 300ms; | |
85 | } | |
86 | ||
87 | .sensitive:hover .cover { | |
88 | opacity: 0; | |
89 | pointer-events: none; | |
90 | } | |
91 | ||
92 | pre { | |
93 | font-size: 2em; | |
94 | font-weight: bold; | |
95 | margin: 0; | |
96 | } | |
97 | pre.spy { | |
98 | color: #c33; | |
99 | } | |
100 | ||
101 | a { color: inherit; } | |
102 | </style> | |
103 | </head> | |
104 | <body> | |
105 | <main> | |
106 | <header> | |
107 | <h2>Fake Artist</h2> | |
108 | <div id="head"></div> | |
109 | </header> | |
110 | <div id="setup" class="inner"> | |
111 | <div>spies: <input id="spies" type="number" value="1" /></div> | |
112 | <div>players:</div> | |
113 | <ul id="players"></ul> | |
114 | <div> | |
115 | <button id="add">add</button> | |
116 | <input id="name" type="text" /> | |
117 | </div> | |
118 | </div> | |
119 | <div id="main"></div> | |
120 | </main> | |
121 | <script src=":text/javascript"></script> | |
122 | </body> | |
123 | </html> |
0 | const words = [ | |
1 | "airplane", | |
2 | "alive", | |
3 | "alligator", | |
4 | "angel", | |
5 | "ant", | |
6 | "apple", | |
7 | "arm", | |
8 | "baby", | |
9 | "backpack", | |
10 | "ball", | |
11 | "balloon", | |
12 | "banana", | |
13 | "bark", | |
14 | "baseball", | |
15 | "basketball", | |
16 | "bat", | |
17 | "bathroom", | |
18 | "beach", | |
19 | "beak", | |
20 | "bear", | |
21 | "bed", | |
22 | "bee", | |
23 | "bell", | |
24 | "bench", | |
25 | "bike", | |
26 | "bird", | |
27 | "blanket", | |
28 | "blocks", | |
29 | "boat", | |
30 | "bone", | |
31 | "book", | |
32 | "bounce", | |
33 | "bow", | |
34 | "bowl", | |
35 | "box", | |
36 | "boy", | |
37 | "bracelet", | |
38 | "branch", | |
39 | "bread", | |
40 | "bridge", | |
41 | "broom", | |
42 | "bug", | |
43 | "bumblebee", | |
44 | "bunk bed", | |
45 | "bunny", | |
46 | "bus", | |
47 | "butterfly", | |
48 | "button", | |
49 | "camera", | |
50 | "candle", | |
51 | "candy", | |
52 | "car", | |
53 | "carrot", | |
54 | "cat", | |
55 | "caterpillar", | |
56 | "chair", | |
57 | "cheese", | |
58 | "cherry", | |
59 | "chicken", | |
60 | "chimney", | |
61 | "clock", | |
62 | "cloud", | |
63 | "coat", | |
64 | "coin", | |
65 | "comb", | |
66 | "computer", | |
67 | "cookie", | |
68 | "corn", | |
69 | "cow", | |
70 | "crab", | |
71 | "crack", | |
72 | "crayon", | |
73 | "cube", | |
74 | "cup", | |
75 | "cupcake", | |
76 | "curl", | |
77 | "daisy", | |
78 | "desk", | |
79 | "diamond", | |
80 | "dinosaur", | |
81 | "dog", | |
82 | "doll", | |
83 | "door", | |
84 | "dragon", | |
85 | "dream", | |
86 | "drum", | |
87 | "duck", | |
88 | "ear", | |
89 | "ears", | |
90 | "Earth", | |
91 | "egg", | |
92 | "elephant", | |
93 | "eye", | |
94 | "eyes", | |
95 | "face", | |
96 | "family", | |
97 | "feather", | |
98 | "feet", | |
99 | "finger", | |
100 | "fire", | |
101 | "fish", | |
102 | "flag", | |
103 | "float", | |
104 | "flower", | |
105 | "fly", | |
106 | "football", | |
107 | "fork", | |
108 | "frog", | |
109 | "ghost", | |
110 | "giraffe", | |
111 | "girl", | |
112 | "glasses", | |
113 | "grapes", | |
114 | "grass", | |
115 | "hair", | |
116 | "hamburger", | |
117 | "hand", | |
118 | "hat", | |
119 | "head", | |
120 | "heart", | |
121 | "helicopter", | |
122 | "hippo", | |
123 | "hook", | |
124 | "horse", | |
125 | "house", | |
126 | "ice cream cone", | |
127 | "inchworm", | |
128 | "island", | |
129 | "jacket", | |
130 | "jail", | |
131 | "jar", | |
132 | "jellyfish", | |
133 | "key", | |
134 | "king", | |
135 | "kite", | |
136 | "kitten", | |
137 | "knee", | |
138 | "ladybug", | |
139 | "lamp", | |
140 | "leaf", | |
141 | "leg", | |
142 | "legs", | |
143 | "lemon", | |
144 | "light", | |
145 | "line", | |
146 | "lion", | |
147 | "lips", | |
148 | "lizard", | |
149 | "lollipop", | |
150 | "love", | |
151 | "man", | |
152 | "Mickey Mouse", | |
153 | "milk", | |
154 | "mitten", | |
155 | "monkey", | |
156 | "monster", | |
157 | "moon", | |
158 | "motorcycle", | |
159 | "mountain", | |
160 | "mountains", | |
161 | "mouse", | |
162 | "mouth", | |
163 | "music", | |
164 | "nail", | |
165 | "neck", | |
166 | "night", | |
167 | "nose", | |
168 | "ocean", | |
169 | "octopus", | |
170 | "orange", | |
171 | "oval", | |
172 | "owl", | |
173 | "pants", | |
174 | "pen", | |
175 | "pencil", | |
176 | "person", | |
177 | "pie", | |
178 | "pig", | |
179 | "pillow", | |
180 | "pizza", | |
181 | "plant", | |
182 | "popsicle", | |
183 | "purse", | |
184 | "rabbit", | |
185 | "rain", | |
186 | "rainbow", | |
187 | "ring", | |
188 | "river", | |
189 | "robot", | |
190 | "rock", | |
191 | "rocket", | |
192 | "sea", | |
193 | "seashell", | |
194 | "sheep", | |
195 | "ship", | |
196 | "shirt", | |
197 | "shoe", | |
198 | "skateboard", | |
199 | "slide", | |
200 | "smile", | |
201 | "snail", | |
202 | "snake", | |
203 | "snowflake", | |
204 | "snowman", | |
205 | "socks", | |
206 | "spider", | |
207 | "spider web", | |
208 | "spoon", | |
209 | "stairs", | |
210 | "star", | |
211 | "starfish", | |
212 | "suitcase", | |
213 | "sun", | |
214 | "sunglasses", | |
215 | "swimming pool", | |
216 | "swing", | |
217 | "table", | |
218 | "tail", | |
219 | "train", | |
220 | "tree", | |
221 | "truck", | |
222 | "turtle", | |
223 | "water", | |
224 | "whale", | |
225 | "wheel", | |
226 | "window", | |
227 | "woman", | |
228 | "worm", | |
229 | "zebra", | |
230 | "zoo", | |
231 | ]; | |
232 | ||
233 | const main = document.getElementById('main'); | |
234 | const head = document.getElementById('head'); | |
235 | const players = document.getElementById('players'); | |
236 | const spies = document.getElementById('spies'); | |
237 | const name = document.getElementById('name'); | |
238 | const add = document.getElementById('add'); | |
239 | ||
240 | const shuffle = (array, rng) => { | |
241 | for (let i = array.length - 1; i > 0; i--) { | |
242 | let j = Math.floor(rng() * (i + 1)); | |
243 | [array[i], array[j]] = [array[j], array[i]]; | |
244 | } | |
245 | }; | |
246 | ||
247 | const infect = a => { | |
248 | a.onclick = () => { | |
249 | const hash = a.href.split('#')[1]; | |
250 | update('#' + hash); | |
251 | }; | |
252 | }; | |
253 | ||
254 | const genSeed = rng => ( | |
255 | words[Math.floor(rng() * words.length)].replace(' ', '-').toLowerCase() | |
256 | + '_' + words[Math.floor(rng() * words.length)].replace(' ', '-').toLowerCase() | |
257 | + '_' + words[Math.floor(rng() * words.length)].replace(' ', '-').toLowerCase() | |
258 | ); | |
259 | ||
260 | const update = (hash) => { | |
261 | const [ mode, seed, n, ...names ] = hash.split(/,/); | |
262 | const rng = new Math.seedrandom(seed); | |
263 | const nextSeed = genSeed(rng); | |
264 | ||
265 | while (main.lastChild) | |
266 | main.removeChild(main.lastChild); | |
267 | ||
268 | while (head.lastChild) | |
269 | head.removeChild(head.lastChild); | |
270 | ||
271 | if (mode === '#link') { | |
272 | const code = document.createElement('code'); | |
273 | code.innerText = seed; | |
274 | head.append(code); | |
275 | ||
276 | document.body.className = ''; | |
277 | names.forEach((name, i) => { | |
278 | const a = document.createElement('a'); | |
279 | a.innerText = 'play'; | |
280 | a.href = `#play,${seed},${n},${names.join(',')},${i}`; | |
281 | infect(a); | |
282 | ||
283 | const div = document.createElement('div'); | |
284 | div.append(`${name}: `); | |
285 | div.append(a); | |
286 | main.append(div); | |
287 | }); | |
288 | ||
289 | const a = document.createElement('a'); | |
290 | a.innerText = 'next round'; | |
291 | a.href = `#link,${nextSeed},${n},${names.join(',')}`; | |
292 | infect(a); | |
293 | main.append(document.createElement('br')); | |
294 | main.append(a); | |
295 | } else if (mode === '#play') { | |
296 | const code = document.createElement('code'); | |
297 | code.innerText = seed; | |
298 | head.append(code); | |
299 | ||
300 | document.body.className = ''; | |
301 | const i = names.pop(); | |
302 | ||
303 | const word = words[Math.floor(rng() * words.length)]; | |
304 | const list = names.map((x, i) => i < +n); | |
305 | shuffle(list, rng); | |
306 | ||
307 | const div = document.createElement('div'); | |
308 | div.className = 'sensitive'; | |
309 | ||
310 | if (list[+i]) { | |
311 | const span = document.createElement('pre'); | |
312 | span.className = 'spy'; | |
313 | span.innerText = 'SPY!'; | |
314 | div.append("you are a"); | |
315 | div.append(document.createElement('br')); | |
316 | div.append(span); | |
317 | } else { | |
318 | const span = document.createElement('pre'); | |
319 | span.innerText = word; | |
320 | div.append("the word is"); | |
321 | div.append(document.createElement('br')); | |
322 | div.append(span); | |
323 | } | |
324 | ||
325 | const cover = document.createElement('div'); | |
326 | cover.className = 'cover'; | |
327 | cover.innerText = '(hover here)'; | |
328 | cover.innerText = `${names[i]}, please hover here to read.`; | |
329 | div.append(cover); | |
330 | ||
331 | const a = document.createElement('a'); | |
332 | a.innerText = 'next round'; | |
333 | a.href = `#play,${nextSeed},${n},${names.join(',')},${i}`; | |
334 | infect(a); | |
335 | ||
336 | main.append(div); | |
337 | main.append(document.createElement('br')); | |
338 | main.append(a); | |
339 | } else { | |
340 | document.body.className = 'setup'; | |
341 | const seed = genSeed(Math.random); | |
342 | const names = []; | |
343 | const a = document.createElement('a'); | |
344 | a.innerText = 'start'; | |
345 | a.href = `#link,${seed},${spies.value},${names.join(',')}`; | |
346 | main.append(a); | |
347 | infect(a); | |
348 | ||
349 | add.onclick = () => { | |
350 | if (!name.value) | |
351 | return; | |
352 | ||
353 | const li = document.createElement('li'); | |
354 | li.innerText = name.value; | |
355 | players.append(li); | |
356 | names.push(name.value); | |
357 | a.href = `#link,${seed},${spies.value},${names.join(',')}`; | |
358 | name.value = ''; | |
359 | }; | |
360 | ||
361 | spies.onchange = () => { | |
362 | a.href = `#link,${seed},${spies.value},${names.join(',')}`; | |
363 | } | |
364 | } | |
365 | } | |
366 | ||
367 | update(window.location.hash); |
0 | a slightly psychedelic physics puzzler with gary, a green-legged giraffe. |
0 | a labyrinth game concering medialisation and multiple viewpoints. developed with the [ForChange research alliance](http://www.forchange.de/ergebnisse/resilienzspiele/). |
0 | a QWOP-y platformer in which you play a room. |
0 | [Ludum Dare 37](http://ludumdare.com/compo/ludum-dare-37/?action=preview&uid=28620) |
0 | a sound-only breakout game, displayable on an oscilloscope and realized in the PureData visual programming environment. |
0 | https://s-ol.itch.io/plonat-atek |
0 | [Ludum Dare 38](https://ldjam.com/events/ludum-dare/38/plonat-atek) |
0 | photo by [Rick Hoppmann](https://twitter.com/tinyruin) |
0 | import div from require 'mmm.dom' | |
1 | import embed from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | ||
3 | => | |
4 | images = for child in *@children | |
5 | embed child, nil, nil, attr: { | |
6 | style: { | |
7 | height: '15em' | |
8 | margin: '0 .5em' | |
9 | flex: '1 0 auto' | |
10 | } | |
11 | } | |
12 | ||
13 | div with images | |
14 | .style = { | |
15 | display: 'flex' | |
16 | overflow: 'auto hidden' | |
17 | } |
0 | # <mmm-embed nolink facet="title"></mmm-embed> | |
1 | ||
2 | <mmm-embed nolink path="pictures"></mmm-embed> | |
3 | ||
4 | *Plonat Atek* is a digital game that communicates itself to the player only via the stereo headphone jack. | |
5 | The signal is split, and one of the streams is fed into a pair of headphones that show the game to the player as a melody, | |
6 | interlaced with a series of blips and bursts of noise. | |
7 | The other stream leads into an oscilloscope, that visualises the two channels by moving a dot across the screen in correspondence to the signal. | |
8 | On the screen, the game manifests as a ball bouncing around in a circular version of *Breakout*. | |
9 | ||
10 | Both the oscilloscope and the speakers are analog devices that interpret the signal and generate a representation of the game. | |
11 | The representations are based on interrelated properties of the same signal. | |
12 | Thus, the relationship between the audio and visual components is not merely designed, rather the two emerge from identical information: | |
13 | In *Plonat Atek*, the sound is designed to be seen and the visuals are designed to be heard. | |
14 | For example the blip heard when the ball bounces is the distortion seen in the same moment, | |
15 | and the noise heard when the ball is lost is visible as a glitch on the oscilloscope. | |
16 | ||
17 | <mmm-embed nolink path="video"></mmm-embed> | |
18 | ||
19 | ## Awards and Exhibitions | |
20 | *Plonat Atek* was awarded first place in the *Innovation* category, 8th in *Audio* and 24th overall at the *Ludum Dare 38 Compo* in 2017. | |
21 | The project has been exhibited at *A MAZE. Berlin 2018* and *Maker Faire Rome 2019*. | |
22 | ||
23 | ## History | |
24 | *Plonat Atek* was originally developed in 48 hours for the *Ludum Dare 38 Compo* in June 2017. | |
25 | You can find the original submission [here][ld] and documentation on how to download and run the jam version on the [itch.io page][itch]. | |
26 | ||
27 | Articles on the game accompanied by the video recording above have been published by [Hackaday][hackaday], [VICE Motherboard][motherboard] and others. | |
28 | ||
29 | The jam version is designed to run on an end-user Computer and is played using the keyboard. | |
30 | In early 2018 I decided to build a self-contained hardware version that is played using a rotary knob. | |
31 | This is the version pictured above. | |
32 | ||
33 | ## Artist Statement | |
34 | *Plonat Atek* is an exploration into the unification of audiovisual signals in the context of feedback in interactive systems. | |
35 | Both digital and analog audio signals are very thin encodings, | |
36 | as they map directly to the vibrations on our tympana and thereby the sensations they represent. | |
37 | In contrast, raster video signals or image encodings are a very contrived, optimized and complicated; | |
38 | the discrete and serial pixel-per-pixel description of shapes does not come close at all to the way our human perception works. | |
39 | This is also obvious as the technology required to decode a video signal or image into something a human can perceive is extremely complex. | |
40 | In *Plonat Atek*, the visual output on the oscilloscope is as directly based on the waveforms encoding it as the audio is. | |
41 | The thinner encoding allows the simultaneous use of the exact same signal for both the audio output on the speakers and | |
42 | the visual output on the oscilloscope, thereby intrinsically connecting the two. | |
43 | ||
44 | At the same time Plonat Atek is a media archaeological project and hommage. | |
45 | The earliest video games, such as *Tennis for Two* and *Spacewar!*, as well as later arcade games like *Asteroids*, | |
46 | and even consoles like the *Vectrex* share the CRT screen whose warm glow transports a unique feeling of messy analogue-ness | |
47 | coupled with a homely sense of healthy imperfection. | |
48 | The game itself is a reinterpretation of the classic *Breakout*, a game many players may recognize nostalgically and | |
49 | that has already gone through transformations from hardware to software, and from an arcade to a pc and finally a mobile game | |
50 | or something found on a children’s toy that comes free with an order at a fast-food restaurant. | |
51 | ||
52 | [ld]: https://ldjam.com/events/ludum-dare/38/plonat-atek | |
53 | [itch]: https://s-ol.itch.io/plonat-atek | |
54 | ||
55 | [hackaday]: https://hackaday.com/2017/11/05/programming-an-oscilloscope-breakout-game-in-pure-data/ | |
56 | [motherboard]: https://motherboard.vice.com/en_us/article/59yw9z/watch-this-awesome-oscilloscope-breakout-game |
0 | https://www.youtube.com/watch?v=SIQAk9_nc-s |
0 | import div, span, h3, ul, li, a, h4, img, p from require 'mmm.dom' | |
1 | import link_to from (require 'mmm.mmmfs.util') require 'mmm.dom' | |
2 | import ropairs from require 'mmm.ordered' | |
3 | ||
4 | => | |
5 | div { | |
6 | h3 link_to @ | |
7 | ul do | |
8 | games = { (p\gett 'date: time/unix'), p for p in *@children } | |
9 | ||
10 | children = for k, child in ropairs games | |
11 | desc = child\gett 'description: mmm/dom' | |
12 | jam = if link = child\get 'jam: mmm/dom' | |
13 | span '[', link, ']', style: float: 'right', clear: 'right', color: 'var(--gray-dark)' | |
14 | ||
15 | li (link_to child), ': ', desc, jam | |
16 | ||
17 | children | |
18 | -- ul with for child in *@children | |
19 | -- link_if_content = (opts) -> | |
20 | -- a with opts | |
21 | -- if true or child\find 'mmm/dom' | |
22 | -- .style = { 'text-decoration': 'none' } | |
23 | -- .href = child.path | |
24 | -- .onclick | |
25 | -- | |
26 | -- li link_if_content { | |
27 | -- h4 { | |
28 | -- style: { 'margin-bottom': 0 } | |
29 | -- (child\get 'title: mmm/dom') or child\gett 'name: alpha' | |
30 | -- } | |
31 | -- div { | |
32 | -- -- style: { | |
33 | -- -- display: 'flex' | |
34 | -- -- 'justify-content': 'space-around' | |
35 | -- -- } | |
36 | -- -- img src: child\gett 'icon: URL -> image/.*' | |
37 | -- p (child\gett 'description: mmm/dom'), style: { 'flex': '1 0 0', margin: '1em' } | |
38 | -- } | |
39 | -- } | |
40 | -- | |
41 | -- .style = { | |
42 | -- 'list-style': 'none' | |
43 | -- } | |
44 | } |
0 | a top down action brawler with a twist. |
0 | [Ludum Dare 33](http://ludumdare.com/compo/ludum-dare-33/?action=preview&uid=28620) |
0 | https://s-ol.itch.io/the-monster-within |
0 | a series of Point-and-Click minigames with a common structure. |
0 | [Ludum Dare 42](https://ldjam.com/events/ludum-dare/42/sacculos-the-game) |
0 | https://itch.io/c/367008/the-sacculi |
0 | a narrative point-and-click adventure. |
0 | http://www.colognegamelab.de/studentprojects/i-looked-at-the-sky-and-saw-two-shooting-stars-but-couldnt-come-up-with-a-wish-2016/ |
0 | I looked at the sky and saw two shooting stars but couldn't come up with a wish |
0 | a puzzle game based on a famicase cartridge design. |
0 | [AGBIC 2016](https://itch.io/jam/a-game-by-its-cover-2016) |
0 | https://s-ol.itch.io/vision-training-kit |
0 | a small reaction/dexteriy game about painting zebras. |
0 | [AGBIC 2018](https://itch.io/jam/a-game-by-its-cover-2018) |
0 | https://s-ol.itch.io/zebra-painting |
3 | 3 | => div { |
4 | 4 | style: { 'max-width': '700px' } |
5 | 5 | h3 link_to @ |
6 | p "mmm is a collection of Lua/Moonscript modules for web development. | |
7 | All modules are 'polymorphic' - they can run in the ", (i 'browser'), | |
6 | p "mmm.dom and mmm.component are Lua/Moonscript modules for web development. | |
7 | Both modules are 'polymorphic' - they can run in the ", (i 'browser'), | |
8 | 8 | ", using the native browser API for creating and interacting with DOM content, as well as on the ", |
9 | 9 | (i 'server'), ", where they operate on and produce equivalent HTML strings." |
10 | 10 | p "As the two implementations of each module are designed to be compatible, |
+0
-1