Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
Tags

Internet Janitor

598
Posts
39
Topics
1,437
Followers
16
Following
A member registered Aug 02, 2018 · View creator page →

Creator of

Recent community posts

There isn't really an observable difference in the behavior of patterns 1 and 47 apart from the fact that 1 is "a texture" which can be inverted and 47 is a "a color" which cannot. I suppose it's sort of a puzzling appendix from a certain point of view. Pattern 47 cannot be selected to draw with manually in either color or 1-bit mode; the "white" swatch on the toolbars normally selects pattern 0, but in "Transparency Mask" mode the same swatch is pattern 32, while erasing or right-click fills still produce pattern 0.

It is entirely possible to use multiple solid colors in animation sequences. I demonstrate this in the demo clip of the AnimEdit contraption; I'll add a brief note to make this clearer.

I've collected a wide range of information about Decker's approach to colors and 1-bit patterns, working with palettes, and importing images, (along with a handy module named "col") in a new example deck:

http://beyondloom.com/decker/color.html

Did you learn anything you didn't previously know? Are any examples unclear? Are there related topics you feel I omitted? I'd love to hear what you folks have to say about it!

The difference is that deck parts are "Interface" values, while your later example is a dictionary.

Lil's basic datatypes (numbers, strings, lists, dictionaries, tables, functions) are all immutable values: attempting to modify them will return a new value and leave the original value unchanged.

Interfaces (like most deck parts) are mutable, and accessing or modifying their attributes may have side-effects. For example, modifying the ".index" attribute of a widget to change its order on a card will also visibly modify the ".widgets" property of the container card, since these attributes are related to one another. The "sys" interface exposes "sys.now", an attribute which contains the current Unix timestamp, and may (generally) be different every time it's accessed. Lil also offers utility Interfaces like Array for those times when mutable data structures are necessary for performance reasons.

Does that make sense?

If there's a material reason that someone needs vector-oriented objects in a deck, they could build a Contraption to represent them. Here's a simple polygon contraption with a configurable fill pattern and a perimeter given as a list of points in a (-1,1) range, with (0,0) centered within the contraption's bounding box:

Ready to paste into a deck:

%%WGT0{"w":[{"name":"polygon1","type":"contraption","size":[62,68],"pos":[225,137],"show":"transparent","def":"polygon","widgets":{"c":{"size":[62,68],"show":"transparent"},"p":{"size":[100,17],"pos":[87,6],"value":39},"s":{"size":[100,14],"pos":[87,32],"value":"[[0.263,-0.142],[0,-0.968],[0.828,-0.593]]"}}}],"d":{"polygon":{"name":"polygon","size":[100,100],"resizable":1,"margin":[1,1,1,1],"script":"on get_pattern do p.value end\non set_pattern x do p.value:x view[] end\non get_shape do s.text end\n\non set_shape x do\n if \"string\"~typeof x x:\"%J\" parse x end\n s.data:x\n view[]\nend\n\non view do\n card.show:\"transparent\"\n c.show:card.show\n sz:card.size/2\n sh:s.data\n sh:sh,list first sh\n sh:flip sz+sz*flip sh\n c.clear[]\n c.pattern:p.value\n c.poly[sh]\n c.pattern:1\n c.brush:1\n c.line[sh]\nend","attributes":{"name":["pattern","shape"],"label":["Pattern","Shape"],"type":["number","code"]},"widgets":{"c":{"type":"canvas","size":[100,100],"pos":[0,0],"locked":1,"volatile":1,"border":0,"brush":1,"scale":1},"p":{"type":"slider","size":[100,25],"pos":[125,9],"show":"none","interval":[0,47],"style":"compact"},"s":{"type":"field","size":[100,20],"pos":[125,47],"show":"none","style":"plain","value":"[]"}}}}}

The contaption uses an internal "volatile" canvas whose bitmap is regenerated on view[] and not saved with the deck. The script is reasonably straightforward. Note how "canvas.lines[]", "canvas.poly[]", and Lil's intrinsic list-conforming behavior allow us to carry out coordinate system transforming and rendering without the need to write any explicit loops!


And in the above example, polygons are constructed on the fly like so:

on click do
 p:card.add["contraption" "polygon"]
 p.size:30+random[40 2]
 p.pos:.5*card.size-p.size
 p.pattern:random[48]
 p.shape:2 window .001*-1000+random[2000 6]
end

If anyone is looking for an interesting project, it could be quite helpful to have a module for interpreting SVG path syntax and flattening it out into simple polylines.

You can use a similar workflow with a wigglyPlayer contraption if you want; Here's an example script you could place in a button to scoop a sequence of frames out of the card background based on the boundaries of some widget and insert them in a wigglyPlayer as an animation value, preserving the original frame order:

on build_anim source target do
 fs:source.parent.image.copy[source.pos source.size]
 fc:floor source.size/target.size
 r.order:target.value.order
 r.frames:each i in range prod fc
  fs.copy[target.size*floor(fc[0]%i),(i/fc[0]) target.size]
 end
 r
end
on click do
 t:some_player
 t.value:build_anim[some_outline t]
end

The built-in function "write[]" can save a Decker sound clip as a .WAV file.

If you have a sound named "sosumi" (like in the default guided tour deck) you could export it by typing the following in the Listener:

write[deck.sounds.sosumi]

If you want to export lots of sounds, it wouldn't be that hard to make a tool to automate the process.

If you're trying to export sounds because you want to use them in a different deck, you can skip the export process and use the Font/DA Mover to import sounds or other bits and pieces directly into a new deck.

Does any of that help?

Thanks for sharing- this looks fantastic!

If you'd like to override the default behavior of the arrow keys, you can define a deck-level (or card-level) replacement for the "navigate[]" event handler. The default handler looks like:

on navigate x do
 if x~"right" go["Next"] end
 if x~"left"  go["Prev"] end
end

Replacing it with an empty handler will make the arrow keys do nothing while in Interact mode:

on navigate do
end

In your case, you've already set up buttons all over your deck with WASD shortcuts, so we could also write a deck-level replacement navigate[] handler which searches for and "clicks" those buttons appropriately in response to cursor keys. On touch devices this should also allow you to navigate by using swipe gestures!

on navigate x do
 s:(("left","right","up","down") dict "adws")[x]
 w:first extract value where value..shortcut=s from deck.card.widgets
 w.event["click"]
end

How's that? There's a bit more detail on navigate[] and other events in the reference manual

And as another option, an interactive version that can go inside your decks, if you want to for some reason!

%%WGT0{"w":[{"name":"nowButton","type":"contraption","size":[88,31],"pos":[264,124],"show":"transparent","def":"animButton","widgets":{"c":{"size":[88,31],"show":"transparent"},"b":{"size":[88,31]},"ims":{"size":[100,6],"pos":[109,2],"value":{"text":["","i","\n","i","\n","i"],"font":["","","","","",""],"arg":["","%%IMG2AFgAHwABAVYAAQECIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEEIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEGIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQEgAQEBIAEBASABAQYgAQEBIFEBBiABAQEgCwEFIEABBSABAQEgCQEDIAUBAyAQAQUgDwECIBcBBiABAQEgBQEDIAsBAyANAQIgAgECIA4BAiAXAQUgAQEBIAQBAiARAQIgCwECIAIBAiACAQQgAwEDIAIBAiACAQIgAgEEIAIBAiABAQIgBgEGIAEBASACAQEgFQEBIAoBAiACAQIgAQECIAIBAiABAQIgAgEBIAEBAiABAQIgAgECIAIBAiABAQMgCAEFIAEBASADAQMgEQEDIAoBAiACAQIgAQECIAIBAiABAQIgBAEEIAMBAiACAQIgAQECIAkBBiABAQEgAgECIAEBAyALAQMgAgEBIAoBAiACAQIgAQEGIAEBAiAEAQMgBAEGIAEBAiAJAQUgAQEBIAMBASABAQEgAQEBIAEBAyAFAQMgBQEBIAoBAiACAQIgAQECIAUBAiAEAQQgAwECIAUBAiAJAQYgAQEBIAIBBCABAQEgAQEBIAEBBSAGAQMgCgECIAIBAiABAQIgAwEBIAEBAiACAQEgAQECIAEBAiACAQIgAwEBIAEBAiAJAQUgAQEBIAMBASABAQUgAQEBIAEBASAGAQMgAgEBIAoBBSADAQQgAwEDIAIBAiACAQIgAgEEIAIBAiAJAQYgAQEBIAIBAiABAQEgAQEFIAEBASACAQMgBQEBIDcBBSABAQEgAwEDIAEBASABAQEgAQEGIAYBAyARAQEgBgECIBEBAiAKAQYgAQEBIAIBAiABAQMgAQEBIAEBAiAGAQMgAgEBIBEBAiAEAQIgEgECIAoBBSABAQEgAwEBIAEBASABAQEgAQEDIAEBAiACAQMgBQEBIBABAyADAQMgAgECIAMBAiAEAQMgAQECIAsBBiABAQEgAgEEIAEBASABAQEgAQEFIAYBAyAQAQUgAQECIAIBBCABAQMgBAEGIAsBBSABAQEgAwEBIAEBBSABAQEgAQEBIAYBAyACAQEgEAECIAEBAiABAQIgAQECIAEBAiABAQIgBQECIAIBAiALAQYgAQEBIAMBAyABAQUgAQEBIAIBAyADAQIgEAECIAMBBCABAQIgAQECIAEBASACAQIgAgECIAIBAiALAQUgAQEBIAYBBCABAQYgAwEDIBIBAiADAQMgAgECIAEBAiABAQIgAQECIAEBAiAQAQYgAQEBIAgBBCABAQEgAgEDIBUBASAFAQIgAgEEIAIBByADAQIgDAEFIAEBASAMAQUgFwECIAUBAiADAQIgBAECIAIBAiADAQIgDAEGIAEBASBQAQUgAQEBIFEBBiABAVEgAQEEIAEBUyABAVggAQEBAAEBVgAB","","%%IMG2AFgAHwABAVYAAQEBIAEBVA0BAQMgAQFTDQEBBCABAVANAwEFIFANAwEFIFANAwEFIAsBBSBADQMBBSAIAQMgBQEDIBABBSAPAQIgFw0DAQUgBQEDIAsBAyANAQIgAgECIA4BAiAXDQMBBSADAQIgEQECIAsBAiACAQIgAgEEIAMBAyACAQIgAgECIAIBBCACAQIgAQECIAYNAwEFIAIBASAVAQEgCgECIAIBAiABAQIgAgECIAEBAiACAQEgAQECIAEBAiACAQIgAgECIAEBAyAIDQMBBSACAQMgEQEDIAoBAiACAQIgAQECIAIBAiABAQIgBAEEIAMBAiACAQIgAQECIAkNAwEFIAIBAiABAQMgCwEDIAIBASAKAQIgAgECIAEBBiABAQIgBAEDIAQBBiABAQIgCQ0DAQUgAgEBIAEBASABAQEgAQEDIAUBAyAFAQEgCgECIAIBAiABAQIgBQECIAQBBCADAQIgBQECIAkNAwEFIAIBBCABAQEgAQEBIAEBBSAGAQMgCgECIAIBAiABAQIgAwEBIAEBAiACAQEgAQECIAEBAiACAQIgAwEBIAEBAiAJDQMBBSACAQEgAQEFIAEBASABAQEgBgEDIAIBASAKAQUgAwEEIAMBAyACAQIgAgECIAIBBCACAQIgCQ0DAQUgAgECIAEBASABAQUgAQEBIAIBAyAFAQEgNw0DAQUgAgEDIAEBASABAQEgAQEGIAYBAyARAQEgBgECIBEBAiAKDQMBBSACAQIgAQEDIAEBASABAQIgBgEDIAIBASARAQIgBAECIBIBAiAKDQMBBSACAQEgAQEBIAEBASABAQMgAQECIAIBAyAFAQEgEAEDIAMBAyACAQIgAwECIAQBAyABAQIgCw0DAQUgAgEEIAEBASABAQEgAQEFIAYBAyAQAQUgAQECIAIBBCABAQMgBAEGIAsNAwEFIAIBASABAQUgAQEBIAEBASAGAQMgAgEBIBABAiABAQIgAQECIAEBAiABAQIgAQECIAUBAiACAQIgCw0DAQUgAwEDIAEBBSABAQEgAgEDIAMBAiAQAQIgAwEEIAEBAiABAQIgAQEBIAIBAiACAQIgAgECIAsNAwEFIAUBBCABAQYgAwEDIBIBAiADAQMgAgECIAEBAiABAQIgAQECIAEBAiAQDQMBBSAIAQQgAQEBIAIBAyAVAQEgBQECIAIBBCACAQcgAwECIAwNAwEFIAsBBSAXAQIgBQECIAMBAiAEAQIgAgECIAMBAiAMDQMBBSBQDQMBBA1UAQMNVQEDDVUBAQABAVYAAQ==","","%%IMG2AFgAHwABAVYAAQEBIAEBVA0BAQMgAQFTDQEBBCABAVANAwEFIFANAwEFIFANAwEFIAsBBSBADQMBBSAIAQMgBQEDIBABBSAPAQIgFw0DAQUgBQEDIAsBAyANAQIgAgECIA4BAiAXDQMBBSADAQIgEQECIAsBAiACAQIgAgEEIAMBAyACAQIgAgECIAIBBCACAQIgAQECIAYNAwEFIAIBASAVAQEgCgECIAIBAiABAQIgAgECIAEBAiACAQEgAQECIAEBAiACAQIgAgECIAEBAyAIDQMBBSACAQMgEQEDIAoBAiACAQIgAQECIAIBAiABAQIgBAEEIAMBAiACAQIgAQECIAkNAwEFIAIBAiABAQMgCwEDIAIBASAKAQIgAgECIAEBBiABAQIgBAEDIAQBBiABAQIgCQ0DAQUgAgEBIAEBASABAQEgAQEDIAUBAyAFAQEgCgECIAIBAiABAQIgBQECIAQBBCADAQIgBQECIAkNAwEFIAIBBCABAQEgAQEBIAEBBSAGAQMgCgECIAIBAiABAQIgAwEBIAEBAiACAQEgAQECIAEBAiACAQIgAwEBIAEBAiAJDQMBBSACAQEgAQEFIAEBASABAQEgBgEDIAIBASAKAQUgAwEEIAMBAyACAQIgAgECIAIBBCACAQIgCQ0DAQUgAgECIAEBASABAQUgAQEBIAIBAyAFAQEgNw0DAQUgAgEDIAEBASABAQEgAQEGIAYBAyARAQEgBgECIBEBAiAKDQMBBSACAQIgAQEDIAEBASABAQIgBgEDIAIBASARAQIgBAECIBIBAiAKDQMBBSACAQEgAQEBIAEBASABAQMgAQECIAIBAyAFAQEgEAEDIAMBAyACAQIgAwECIAQBAyABAQIgCw0DAQUgAgEEIAEBASABAQEgAQEFIAYBAyAQAQUgAQECIAIBBCABAQMgBAEGIAsNAwEFIAIBASABAQUgAQEBIAEBASAGAQMgAgEBIBABAiABAQIgAQECIAEBAiABAQIgAQECIAUBAiACAQIgCw0DAQUgAwEDIAEBBSABAQEgAgEDIAMBAiAQAQIgAwEEIAEBAiABAQIgAQEBIAIBAiACAQIgAgECIAsNAwEFIAUBBCABAQYgAwEDIBIBAiADAQMgAgECIAEBAiABAQIgAQECIAEBAiAQDQMBBSAIAQQgAQEBIAIBAyAVAQEgBQECIAIBBCACAQcgAwECIAwNAwEFIAsBBSAXAQIgBQECIAMBAiAEAQIgAgECIAMBAiAMDQMBBSBQDQMBBA1UAQMNVQEDDVUBAQABAVYAAQ=="]}},"ai":{"size":[100,7],"pos":[109,11],"value":"8"}}}],"d":{"animButton":{"name":"animButton","size":[100,100],"resizable":1,"margin":[1,1,1,1],"description":"A graphical drop-in replacement for buttons.","script":"on rev x do x @ (-1+count x)-range count x end\n\non view do\n i:extract arg where arg..type=\"image\" from ims.value\n f:first i\n c.show:card.show\n c.clear[]\n if c.animated\n  s:(range count i),1 drop rev[range count i]\n  a:floor ai.text/2\n  f:f unless i[s[a]]\n  if a~(count s)-1 c.animated:b.locked:0 else ai.text:1+ai.text end\n end\n c.paste[f .5*c.size-f.size]\nend\n\non click do\n if !b.locked\n  ai.text:0\n  c.animated:b.locked:1\n  view[]\n  card.event[\"click\"]\n end\nend\n\non get_images do ims.value end\non set_images x do ims.value:x end","template":"on click do\n \nend","attributes":{"name":["images"],"label":["Images"],"type":["rich"]},"widgets":{"c":{"type":"canvas","size":[100,100],"pos":[0,0],"locked":1,"volatile":1,"border":0,"scale":1},"b":{"type":"button","size":[100,100],"pos":[0,0],"show":"transparent","style":"invisible"},"ims":{"type":"field","size":[100,20],"pos":[121,5],"show":"none","border":1},"ai":{"type":"field","size":[100,20],"pos":[121,37],"show":"none","style":"plain"}}}}} 

I thought I'd share a few assorted Decker projects that I haven't posted here before, in hopes that they could provide useful examples or inspiration (or even just be fun to play with!) Feel free to borrow or re-use any of the parts of these decks!

Acrostic

 

A fiendish word-search puzzle with exactly one solution. Try changing the word from "eggbug" to something else! Uses a trial-and-error algorithm to generate boards.

Virtual Bubble Wrap 

Just bubble wrap! Pop bubbles, and then pop some more. No in-app payments, I promise.

Eggbuggin' 

A simple 2048-like puzzle game: use arrow keys to shift the board and combine proto-eggbugs into a complete eggbug! It would be pretty easy to "reskin" this deck with your own drawings and sound effects. This deck is locked by default; you can unlock it by typing

deck.locked=false

in your browser's JavaScript console!

I'm also happy to answer any questions about how these projects work.

Done!

(2 edits)

Every July and December we hold official Decker-themed game jams here on itch.io:

(Maybe we could even do these more often if there's enough interest?)

The next scheduled Jam will be Dec(k) Month The Second, December 2024; I'll update this post with the link when it's ready.

If you're participating in or hosting another jam where Decker might be a good fit, feel free to drop a link in this thread to let us know. Remember to tag your creations with #decker so your fellow deckbuilders can find them!

(1 edit)

This forum is probably the best place to find that kind of info. The general plan, until something changes, is to hold two month-long jams every year:

  • Decker Fantasy Camp (July)
  • Dec(k) Month (December)

I will also mention upcoming jams in Decker release notes.

I'm open to other methods of handling announcements if folks have suggestions!

Be sure to let us know how the project goes!

(1 edit)

It would take a little bit of refactoring of lil.js to avoid producing global references, and then you could wrap it all in a closure to expose a tidy public interface. Removing some parts of the Decker DOM and utility routines would help make such a distribution smaller (Though you definitely want to retain utility interfaces like RText, Bits, Array, and Image). You'd also want to polyfill certain builtins like show[] and print[] to allow them to be re-vectored into something useful for a given application or to remove them entirely, not unlike the repl.js stub used in the regression test suite.

I'm a bit hesitant to perform this surgery on Decker's main branch, because it would add some complexity and size to web-decker. If this is a project you're interested in pursuing I encourage it!

Currently, Decker is limited to working with US-ASCII text; I apologize for this inconvenience to those who use other alphabets and glyphs.

You can, however, customize any of the fonts (including built-in fonts) to replace a few punctuation characters with glyphs you're missing. The example deck "fontedit" is one way to do this:

 

I also wrote this post describing how to build your own tools for importing bitmapped fonts and converting them into Decker's internal format.

Wow, this is really high-concept. You knocked it out of the park, Millie!

The "image.map[]" function can be used to re-palette any Image interface, including card backgrounds. The simplest way to call it is to feed it a dictionary where the keys are the original pattern indices and the values are the replacement pattern indices:

card.image.map[colors.red dict colors.green]

Note that this operation modifies the image in-place.

Yes; there's even an example in the documentation; see the card script here:

http://beyondloom.com/decker/dialog.html#commands

Variables that live beyond an event handler must be stored in a widget. If you find it inconvenient to reference widgets on another card,

thatCard.widgets.has_knife.value
thatCard.widgets.has_soap.value
thatCard.widgets.has_corncob.value

You could write utility functions on the card script that aggregate information from widgets:

on get_inventory do
 ("knife","soap","corncob") dict (has_knife,has_soap,has_corncob)..value
end

And then send events at that card from elsewhere:

thatCard.event["get_inventory"]

(Or as shorthand if the event handler doesn't take any arguments,)

thatCard.event.get_inventory

In this way you can give cards an "API" of sorts. You could also define helper functions in the deck-level script to reach into specific cards:

on items do
 thatCard.event.get_inventory
end

And then call that helper function from anywhere in the deck:

items[]

(I think this is probably overkill unless you're accessing the same info in lots of places within a deck.)

You can absolutely use tables to keep track of dialog options. A grid widget stores a table in its "value" attribute. Aside from manually editing table contents as CSV or JSON, you can initialize one with a script:

myGrid.value:insert k v with
 "Tell me about Chickens"  "They cool."
 "Tell me about Daffodils" "They flower."
end

You could append a new row (or several rows) to the existing table in a grid like so:

myGrid.value:insert k v with
 "Tell me about Easter Eggs" "They rare."
into myGrid.value

And you could then later retrieve that table and convert it into the dictionary form dd.chat[] wants by razing it (which turns the first two columns into dictionary keys and values, respectively):

dd.open[deck]
dd.chat["Ask away." (raze myGrid.value)]
dd.close[]

Note that if you plan on revisiting the same dialog tree over the course of a game many times but you don't want to repeat old selections you'll need to do additional bookkeeping; dd.chat[] only remembers/winnows selections within the course of one conversation.

For more information about tables and querying, see Lil, The Query Language.

Does that help point you in the right direction?

(1 edit)

Absolutely!

And for anyone else reading, let me be clear: Anyone is more than welcome to make threads to show off their cool projects! If you make anything at all with Decker, we'd love to see it here!

Zine of Millie is truly a delight, especially as a way to look back on how it has evolved along with Decker.

I'm really looking forward to what you're cooking up in issue 22!

In Lil, the "&" and "|" operators provide logical AND and logical OR. Both "conform" to the elements of lists:

(1,1,0,0) & (1,0,1,0)  # -> (1,0,0,0)
(1,1,0,0) | (1,0,1,0)  # -> (1,1,1,0)

To be more precise, these operators are actually binary "minimum" and "maximum" operators for numbers:

(1,2,3,4) & 2          # -> (1,2,2,2)
(1,2,3,4) | 2          # -> (2,2,3,4)

As always, be careful to remember that Lil expressions are carried out right-to-left unless you include parentheses.

For comparisons, Lil has < (less), > (more), and = (equal). It does not have "compound" symbols like "<=", ">=", or "!=". If you want the complementary operation, you can use ! (negation):

  a > b      # a is more than b
! a > b      # a is not more than b (a <= b)
  a < b      # a is less than b
! a < b      # a is not less than b (a >= b)
  a = b      # a is equal to b
! a = b      # a is not equal to be (a != b)

Note that all of these operations work to lexicographically compare strings, too:

"Apple" < "Aardvark","Blueberry"    # -> (0,1)
"Apple" & "Aardvark","Blueberry"    # -> ("Aardvark","Apple")
"Apple" | "Aardvark","Blueberry"    # -> ("Apple","Blueberry")

Lil syntax highlighting profiles for vim, emacs, and Sublime Text are available at the Decker Github Repo.

Syntax highlighting is also available in the Lil Playground.

OK, let's make a simple Colossal-Cave-Adventure-style two-word parser.

Lil's parsing patterns can get a little bit cryptic, but essentially what we want to break up user input is:

  • Skip any leading spaces.
  • Grab any characters up to a space or newline and call them "verb".
  • Skip any spaces.
  • Grab any characters up to a newline and call them "obj".

Which can be expressed as the pattern:

pat:"%*r %[verb]-.2r \n%*r %[obj]s\n"

I did a few tests in the Listener to make sure it was working as intended:

 pat parse "foo"
{"verb":"foo","obj":""}
 pat parse "  foo"
{"verb":"foo","obj":""}
 pat parse "foo bar"
{"verb":"foo","obj":"bar"}
 pat parse "foo bar\nbaz"
{"verb":"foo","obj":"bar"}
 pat parse "foo  bar\nbaz"
{"verb":"foo","obj":"bar"}

Now we can rework the input field's change[] handler to bundle this up and send the verb/object to the card script:

on change do
 if "\n" in me.text
  c:"%*r %[verb]-.2r \n%*r %[obj]s\n" parse me.text
  me.text:""
  typed[c.verb c.obj]
 end
end

In the card script, we'll have our actual game logic, a twisty maze of if statements:

on println x do
 log.text:log.text,x,"\n"
 log.scroll:999999
end
on typed verb obj do
 println["> %s %s" format verb,obj]
 if verb~"look"
  if obj~"flask"
   println["it is a flask."]
  elseif obj~"self"
   println["that's difficult unless your eyes are prehensile."]
  else
   println["ye see ye flask."]
  end
 elseif verb~"take"
  if obj~"flask"
   println["ye cannot take ye flask."]  
  else
   println["i don't see a %s anywhere here." format obj]
  end
 else
  println["ye is speakin' nonsense."]
 end
end

And our scintillating gameplay experience begins:

(1 edit)

This request got me thinking. I made some minor additions to WigglyKit in order to permit making wigglyPlayer contraptions draggable  just like ordinary Canvas widgets, which allows us to make wiggly stickers!


This example uses the wigglyCanvas contraption for drawing and the wigglyPlayer for stickers; the "Sticker!" button has a script like so:

on click do
 c:card.add["contraption" "wigglyPlayer"]
 c.draggable:1
 c.size:source.size
 c.value:source.value
 c.pos:(card.size-c.size)/2
end

Here's an example with all the parts needed that you can copy and paste onto a blank card:

%%WGT0{"w":[{"name":"button1","type":"button","size":[51,20],"pos":[371,197],"script":"on click do\n c:card.add[\"contraption\" \"wigglyPlayer\"]\n c.draggable:1\n c.size:source.size\n c.value:source.value\n c.pos:(card.size-c.size)/2\nend","text":"Sticker!"},{"name":"source","type":"contraption","size":[131,89],"pos":[354,102],"def":"wigglyCanvas","widgets":{"c":{"size":[131,89],"brush":2},"fr":{"size":[131,20],"value":{"text":["i","i","i"],"font":["","",""],"arg":["%%IMG2AIMAWQD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wC4","%%IMG2AIMAWQD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wC4","%%IMG2AIMAWQD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wC4"]}},"sfx":{"size":[131,31]},"tb":{"size":[79,20],"pos":[0,137]},"cbo":{"size":[100,18],"pos":[185,0]},"cp":{"size":[100,18],"pos":[185,28]},"cbr":{"size":[100,18],"pos":[185,56],"value":"2"}}},{"name":"clear","type":"button","size":[40,20],"pos":[432,197],"script":"on click do\n source.clear[]\nend","text":"Clear"},{"name":"wigglyPlayer1","type":"contraption","size":[131,89],"pos":[158,117],"def":"wigglyPlayer","widgets":{"c":{"size":[131,89],"draggable":1},"fr":{"size":[131,20],"value":{"text":["i","i","i"],"font":["","",""],"arg":["%%IMG2AIMAWQD/AP8A/wD/AP8A/wDgAQIABAEDAAIBAgB1AQ8AbwECAAEBEgBtARgAagEaAGgBCQADAQMABAEJAGcBBwAPAQYAZgEFABIBBwBlAQUAEgEHAGYBBAATAQUAZwEFABIBBgBnAQUAEQEGAGcBBQAEAQgAAQECAAMBBQANAQIAWAEVAAEBBgAMAQ0ATwEgAAYBDwBLAQIAAgEgAAQBEgBIAScAAQEUAEYBDQALARYAAQEDAAEBCgBFAQsAEgECAAQBCwALAQYAQwEJABwBCgAMAQUAQgEIAB4BCgANAQYAQAEGACEBAgACAQYADQEHADwBBgAoAQUADgEHADgBCQApAQUADgEGADcBCQArAQUADwEFADYBBwAuAQUADgEGADUBBwAEAQIAKQEFAA0BBwA1AQUAAwEFACgBBgANAQYANAEFAAMBBgApAQYADgEFADIBBgADAQYAHgECAAoBBgAHAQMAAgEGADIBBgACAQYAHgEEAAoBBgAFAQsAMwEFAAMBBAAgAQUACwEFAAEBDgAzAQUABAECACIBBAAMARIANAEFACkBAgANAREANgEFADgBCQABAQMAOgEEADgBBwBAAQUABAECADIBBgBAAQUAAwEEAAkBAwAlAQUAQQEFAAMBBQAHAQUAJAEFAEEBBAAFAQUABQEGACUBBQBAAQQABQEGAAMBBgAmAQUAQAEFAAQBDwAIAQUAGQEFAEABBgAEARMAAQEIABgBBQBBAQYAAwEKAAEBEQAYAQUAQgEGAAMBCAADAQ8AFwEIAEEBBwAEAQMABwENABcBCQBCAQYADwEMABcBCQBDAQUAFgEEABMBDgBEAQYAKgEKAAEBBABEAQcAJwELAAIBBABFAQoAHQEDAAEBDgACAQIARgELAAMBAgAVAQ4AAQEFAEsBEAADAQMADQENAAIBBgBLAS4ABAEGAEwBLgADAQcASgEFAAIBIQABAQYABAEHAEkBBQADARwAAQECAAMBBQAEAQcASAEGAAwBAwADAQIAAQECAAQBAgAJAQUABAEFAEkBBgAnAQQABAEEAEoBBgAmAQUABQECAEsBBQAnAQcAUAEFACcBCABPAQUAKAEHAFABAwArAQQA/wD/AP8A/wD/AP8AVQ==","%%IMG2AIMAWQD/AP8A/wD/AP8A/wDhAQIAAgEHAHQBAgABAQ0AcQETAG8BFQBrARkAaQEMAAEBAgAHAQUAaAEHAAEBAwAMAQUAZwEFABIBBgBmAQUAEwEGAGUBBQAUAQUAZgEFABIBBQBnAQYAEQEGAGcBBQACAQIAAgECAAkBBgAMAQIAWQEFAAEBFwAKAQsAUgEeAAgBDQBQASAABAEQAEwBJAACARMASAEMAAYBBgACARYABQEIAEUBCQATAQQAAQENAAoBBwBDAQkAGgECAAEBCQALAQYAQgEGACIBCQAMAQUAQQEFACQBCAANAQYAPgEFACgBBQAOAQgAOQEHACkBBQAOAQgANwEHACsBBgANAQcANgEHAC0BBgANAQcANAEHAAEBBAAqAQYADgEFADQBDQAqAQUADwEEADQBBgABAQYAKgEGAA4BBQAyAQYAAgEGACABAgAJAQYABwEDAAEBBwAyAQYAAgEGAB8BBAAJAQYABQEMADIBBgACAQUAIAEEAAoBBgACAQ0AMwEFAAQBAgAiAQQACwETADUBBQAoAQIADQEQADcBBQA3AQ8AOQEFADcBCQA+AQUABgECADABBABCAQUABQEEAAYBAgAnAQQAQgEFAAQBBQAFAQQAJgEFAEEBBQAEAQQABgEFACUBBQBBAQUABAEPACUBBQBCAQQABAEQAAgBAwAZAQYAQQEEAAUBDwABAQIABAEFABgBBgBBAQQABQEKAAEBEAAYAQYAQQEFAAUBCAADAQ4AGgEFAEIBBQAFAQIAAgECAAUBDQAZAQYAQwEFAA8BDAAVAQMAAQEHAEQBBAAQAQIAAgEEABcBDQBDAQYAAQECACcBEABDAQoAJQENAAEBAgBFAQkAIgEPAEoBCwABAQMAGQEQAE0BEAABAQIABwECAAQBEAABAQYATAEuAAMBBgBMASwABQEGAEwBLQAEAQcASwEEAAIBAgACAR0AAgEFAAMBBwBKAQUACwEEAAEBBAABAQcACQEGAAMBBwBJAQUAJgEFAAQBBgBJAQUAJgEFAAYBAwBJAQUAJwEGAFEBBQAnAQcATwEGACgBBwBOAQUAKwEFAE4BBAAsAQQAUAECAC4BAgD/AP8A/wD/AP8ATw==","%%IMG2AIMAWQD/AP8A/wD/AP8A/wDhAQIABQECAHgBDgByARIAbwEVAGwBGABqARMAAQEGAGkBBgAQAQUAaAEFABEBBgBnAQQAEgEGAGcBBAASAQcAZgEFABIBBgBmAQUAEgEGAGcBBAADAQYAAwEMAA0BBABWAQUAAQEVAA0BCwBRAR0ACQENAEwBAgACASEABAEQAEkBGQABASIARwEMAAYBAwAGARMABwEIAEQBDQAVAQ4ACQEGAEMBCgAaAQ0ACwEFAEIBCQAfAQgADAEGAEEBBQAmAQUADQEHAD0BBgAoAQUADQEIADoBBwAoAQYADAEJADgBBwAqAQYADAEIADcBBgAtAQYADwEFADUBBwAvAQcADAEGADQBBgACAQQAKgEIAAsBBwAyAQYAAgEGACkBCAAMAQYAMgEGAAIBBgApAQgACQEIADMBBQAEAQUAIAECAAgBAgABAQUABwEKADIBBQAFAQMAIAEEAAsBBAAEAQ0AMgEFACgBBQAKAQUAAgENADMBBQApAQQACgEFAAEBCgA4AQQAKgECAAwBDgA5AQUAOAEKAD0BBAAGAQIAMQEHAD8BBAAFAQQACAEDACUBBgBAAQQABQEEAAYBBgAkAQUAQAEFAAUBBAAFAQcAJQEEAEABBQAEAQYAAQEKAAcBAgAcAQUAQAEEAAQBEgAFAQUAGgEFAEABBgACARIAAwEIABgBBgBBAQYAAgEcABgBBgBCAQUAAwEKAAIBDgAYAQcAQgEGAAQBAgACAQIABQEMABgBCQBCAQUAEAEKABYBDQBCAQUAEQEFABcBDwBDAQYAJwESAEQBBwAkAQ4ASwEHACIBEABLAQgAAwEDABYBFABMAQ8AAgEDAAUBBAAFAQ0AAgEFAE0BLAAFAQYATAEtAAQBBgBMAS8AAwEGAEoBBgADAQIAAwEWAAUBCAADAQUASgEFAAsBAwABAQMAAgECAAQBAgAJAQcAAwEFAEkBBQAoAQUAAwEEAEoBBgAnAQYAAwECAEsBBgAnAQYAUAEFACgBBgBQAQUAKQEGAE8BBQArAQQATwEEAC0BAgBRAQIA/wD/AP8A/wD/AH4="]}},"or":{"size":[131,20],"value":"[0,1,2]"},"fl":{"size":[79,20],"pos":[0,137]},"em":{"pos":[140,-49]},"cbo":{"size":[100,18],"pos":[140,0]},"cst":{"size":[100,17],"pos":[140,28]},"cd":{"size":[100,18],"pos":[140,54],"value":1}}}],"d":{"wigglyCanvas":{"name":"wigglyCanvas","size":[100,100],"resizable":1,"margin":[1,1,1,1],"description":"A simple wiggly drawing surface compatible with wigglyPlayer.","version":1.1,"script":"on frames do fr.images end\non get_value do\n r.frames:frames[]\n r.order:range count r.frames\n r\nend\non set_value x do\n if x.frames fr.images: x.frames end\nend\n\non drag x do\n if !card.locked\n  fr:frames[]\n  c.pattern:get_pattern[]\n  c.brush:get_brush[]\n  o:if pointer.down x else pointer.prev-card.offset end\n  each f in fr\n   c.paste[f]\n   c.line[o random[-1,0,1 2]+x]\n   f.paste[c.copy[]]\n   if tb.value\n    f.paste[deck.card.image.copy[card.pos card.size] 0,0 1]\n   end\n  end\n  if !app.playing\n   play[sound[random[sfx.value.data]]]\n  end\n end\nend\non click x do\n if !card.locked\n  card.event[\"stroke_start\"]\n  drag[x]\n end\nend\non release x do\n if !card.locked\n  card.event[\"stroke_end\"]\n end\nend\n\non view do\n f:frames[]\n c.border:cbo.value\n c.show:card.show\n c.clear[]\n i:f[(count f)%sys.frame/5]\n i.size:c.size\n c.paste[i]\nend\n\non get_animate do view end\non get_border do cbo.value end\non set_border x do c.border:x cbo.value:c.border end\non get_brush do 0+cbr.text end\non set_brush x do c.brush:x cbr.text:c.brush end\non get_pattern do 0+cp.text end\non set_pattern x do c.pattern:x cp.text:c.pattern end\non get_temp do tb.value end\non set_temp x do tb.value:x end\non get_clear do\n on clear do\n  fr.value:raze rtext.cat @ (3 take image[])..copy[]\n end\nend","template":"on stroke_start do\n \nend\n\non stroke_end do\n \nend","attributes":{"name":["border","temp"],"label":["Border","Template Background"],"type":["bool","bool"]},"widgets":{"c":{"type":"canvas","size":[100,100],"pos":[0,0],"animated":1,"volatile":1,"border":1,"brush":1,"scale":1},"fr":{"type":"field","size":[100,20],"pos":[0,-77],"locked":1,"show":"none","value":{"text":["","i","i","i"],"font":["","","",""],"arg":["","%%IMG2AGQAZAD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wA3","%%IMG2AGQAZAD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wA3","%%IMG2AGQAZAD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wD/AP8A/wA3"]}},"sfx":{"type":"grid","size":[100,31],"pos":[0,-49],"locked":1,"show":"none","format":"ss","value":{"name":["pen1","pen2"],"data":["%%SND0B/307ers8vsDDRMWEwwC+fDq6Oz0/wsUGRkSB/vv5uPm8P4MGB8eFQj46eDd4/ECEyAkHxIA7uDa3en8Dh4lJBgG8+La2uX2CBojJB0N++rf3OLvAA4aHhwTB/nv5+fs9P8HDhANCQL//Pv7/P8AAQMC//v49fX2+wAGDBAREA4KA/317uro6u3x+P8FCxAUFxgXEgwE/vbw7Onp6evw9PoABAkNERMVFRQRDAcB/ffy7+zr6+zv8vf7AAQIDRATFBQTEAwIAv769PDt6+vr7fD0+f4DCA0RFBUVFBENCAP++fPv7Orq6u3w9Pr/BAkOERMUFBMRDQgD//r18e7t7Ozu8fT4/P8DBgkLDA0NDAsJBgQDAgD+/fv6+vn5+fr6+/z9/v//AAAAAQIDBAMCAQEAAP/+/vz9/Pz8/Pv8/fz9/f7/AP8AAAAAAgMEBQQEBAQFAwMDBAMCAwICAQABAAAAAQEAAQAAAP/+/v3+/fz9/Pz8/Pz8/Pz8/Pv8/f38/v79////AAAAAAABAQECAQMCAgMEAwMDAwMDAwMDAwQCAwIDAgIBAQIBAAEAAAAAAAD+//7+/f7+/f39/f39/fz7/Pz8/P39/f39/f38/f7+/v7+/f/+","%%SND0AAAA/wAAAAAAAAABAAD////+/wAAAAABAAD////+AAABAAAAAAD+//8AAAAAAAAA/wAAAAAA/wAA/wAAAAECAQD+/fz9/wADBQQB//v4+f0CBggGAvz39vj/BgkJBP749vj/BggIAv35+f0AAwQBAP3+AAAA/v0AAgUEAPr2+AAIDQf+8/D3Aw4RCPnu7vkIEhEE9evu/AsUEQLz6+/8DBQRA/Tr7vkIExQK+u7r8v8NFBIG9+3r8gANFRMJ++/q7/oHEhUQBPbs6vD8CBMVEQX47eru+AUPFBMK/vLr7PP+CRETDgT78u/x+QMMEA4H/fTv8PkCCw8NBf318fT7AwkKCAL8+Pj8AAMEAwD+/wACAgD8+fk="]}},"tb":{"type":"button","size":[60,20],"pos":[0,148],"locked":1,"show":"none","style":"check"},"cbo":{"type":"button","size":[100,20],"pos":[154,0],"locked":1,"show":"none","style":"check","value":1},"cp":{"type":"field","size":[100,20],"pos":[154,32],"locked":1,"show":"none","style":"plain","value":"1"},"cbr":{"type":"field","size":[100,20],"pos":[154,63],"locked":1,"show":"none","style":"plain","value":"3"}}},"wigglyPlayer":{"name":"wigglyPlayer","size":[100,100],"resizable":1,"margin":[1,1,1,1],"description":"A viewer and storage location for wiggly animations, which is also suitable as a \"Fancy Puppet\" for use with Puppeteer.","version":1.2,"script":"on frames do fr.images end\non order  do or.data end\n\non set_value x do\n if x.frames fr.images:x.frames..copy[] end\n or.data: (range count x.frames) unless x.order\nend\non get_value do\n r.frames:frames[]\n r.order :order[]\n r\nend\n\non view do\n f:frames[]\n o:order[]\n c.border:cbo.value\n c.show:card.show\n c.draggable:cd.value\n c.clear[]\n if count f\n  i:f[o[(count o)%sys.frame/5]]\n  i:if fl.value i.copy[].transform[\"horiz\"] else i end\n  if get_stretch[]\n   c.paste[i 0,0,card.size]\n  else\n   c.paste[i .5*c.size-i.size]\n  end\n end\nend\n\non get_border do cbo.value end\non set_border x do c.border:x cbo.value:c.border end\non get_stretch do cst.value end\non set_stretch x do cst.value:x end\non get_frames do fr.value end\non set_frames x do fr.value:x end\non get_order_text do \",\" fuse order[] end\non set_order_text x do or.data: 0+\",\" split x end\non get_animate do view end\non get_flip do fl.value end\non set_flip x do fl.value:x end\non get_emotes do em.data end\non set_emotes x do em.data: x end\non set_emote x do or.data:() unless get_emotes[][x] end\non get_draggable do cd.value end\non set_draggable x do cd.value:x end\n","template":"on click do\n \nend\n\non drag do\n \nend\n\non release do\n \nend","attributes":{"name":["border","draggable","stretch","frames","order_text"],"label":["Border","Draggable","Stretch to Fit","Frames","Frame Order"],"type":["bool","bool","bool","rich","string"]},"widgets":{"c":{"type":"canvas","size":[100,100],"pos":[0,0],"locked":1,"animated":1,"volatile":1,"script":"on click pos do\n card.event[\"click\" pos]\nend\non drag pos do\n if get_draggable[]\n  card.pos:pos+(pointer.pos-pointer.start)\n  me.pos:0,0\n  card.event[\"drag\" pos]\n end\nend\non release pos do\n if get_draggable[]\n  card.event[\"release\" pos]\n end\nend","border":1,"scale":1},"fr":{"type":"field","size":[100,20],"pos":[0,-77],"locked":1,"show":"none"},"or":{"type":"field","size":[100,20],"pos":[0,-49],"locked":1,"show":"none","style":"plain"},"fl":{"type":"button","size":[60,20],"pos":[0,148],"locked":1,"show":"none","style":"check"},"em":{"type":"field","size":[100,20],"pos":[109,-49],"locked":1,"show":"none","style":"plain"},"cbo":{"type":"button","size":[100,20],"pos":[109,0],"locked":1,"show":"none","style":"check","value":1},"cst":{"type":"button","size":[100,20],"pos":[109,31],"locked":1,"show":"none","style":"check","value":1},"cd":{"type":"button","size":[100,20],"pos":[109,61],"locked":1,"show":"none","style":"check","value":0}}}}}

Decker 1.49 includes some subtle changes that I think will address this request.

It is now possible to clear a field via scripts while it is selected. In touch mode, this will additionally dismiss the on-screen keyboard and remove focus from the field.


In the above example, the script looks like this:

on change do
 if "\n" in me.text
  log.text:log.text,"\n %s%J" format me.text,list eval[me.text].value
  log.scroll:99999
  me.text:""
 end
end

There are a few details to be aware of when using this technique:

  • It takes 1/4 of a second without user input for the contents of a field to fire a change[] event, and it is also possible to fire a change[] event by pasting an arbitrary block of text into a field. Therefore it's possible for a user to type several characters following a newline, or even multiple newlines; input handling code will need to be robust to this.
  • An alternative to looking for "\n" in a change[] event would be to use the run[] event, which is fired immediately when the user presses shift+return. This may be less intuitive for users, and therefore unsuitable for many applications, but it's much less error-prone and more flexible than newline-delimited input; the run[] event makes it possible to build UIs which behave like Decker's Listener.
  • Since the user actually does type a newline, you'll need to carefully adjust the vertical sizing of the input field to match the font in order to maintain the illusion that the field is "cleared" instantly when the user presses return.

I hope this helps!

Just to add to this, one way to programmatically obtain thumbnails of cards is to use app.render[]. You can then scale the resulting Image down and paste it onto a draggable canvas. If cards mostly contain the same structure, though, small thumbnails probably won't be very easy to distinguish at a glance.

(1 edit)

In Lil, expressions are evaluated from right to left unless overridden with parentheses. The expression

checkbox1.value + checkbox2.value = 2

Is equivalent to

checkbox2.value + (checkbox2.value = 2)

What you probably mean is

2 = checkbox1.value + checkbox2.value

Which is equivalent to

2 = (checkbox1.value + checkbox2.value)

You might find it slightly simpler to use "&" (minimum/logical AND), which for a pair of 0/1 values will be 1 if and only if both inputs were 1:

checkbox1.value & checkbox2.value

If you have a whole bunch of checkboxes, you could make a list of them, extract each of their values, and take the minimum:

min (c1,c2,c3,c4)..value

You can use Decker's Listener to test out expressions like the above while you work; it's a very handy tool!

It's also worth noting that even if a button isn't visibly a checkbox, it still has a ".value" field like a checkbox, so you could simplify things by tracking clicks in the buttons themselves:

on click do
 me.value:1
 card.event["yeah"]
end 

If the "yeah[]" function is defined on the same card as the widget, (or in the deck script) it can be called directly, instead of triggered through an event (either way is fine. Using events is necessary if you're calling a function defined on a different card or widget):

on click do
 me.value:1
 yeah[]
end

Does that help?

Hmm. I think this would be rather tricky.

Decker is designed to try to accommodate text input on devices that don't have a physical keyboard, so it avoids exposing low-level keyboard events and doesn't (presently) allow scripts to modify the contents of a text field while it has user focus. Consider how the soft keyboard overlay introduced in Decker 1.19 interacts with unlocked field widgets.  While it's easy enough to detect that a user has pressed return in a text field (and typed a newline),

on change do
 if "\n" in me.text
  # ...
 end
end

That event handler wouldn't be able to clear out the user's line of input, which would make a scripting REPL or an IF parser input awkward.

I'll give this some serious thought and see if I can come up with a good compromise to allow the sort of thing you're interested in making

Welcome! I'm delighted to hear that you're having a positive experience expressing yourself with Decker, and I'm glad we can offer you some solace and distraction from whatever you're going through. I originally created Decker while going through a difficult time of my own.

It looks like the script fragment you provide doesn't have the correct Lil syntax, which might cause problems elsewhere. The "on ... do" and "if" keywords always need to be paired with "end", like so:

on view do
 display.text:if val > 0
  display.text:0 
 end
end

I'm also not sure where "val" is coming from in this example. For the sake of my explanation, I'll start from scratch. Let's say that we have a card that looks something like this, with two fields and a canvas:


If we wanted to clear these every time the user reloads the deck, there's one option that doesn't involve any code: you can configure these widgets as "Volatile" (See the Widgets -> Volatile menu item). A volatile field "forgets" its text contents when a deck is saved, and a volatile canvas likewise loses its image contents. You can also manually discard the contents of all volatile widgets in a deck at any time by using File -> Purge Volatiles from the menu. This approach isn't very flexible, though; there isn't a way to simply hide a canvas instead of automatically clearing it, for example. Still, this might be useful for some scenarios.

You could give the card a "view[]" event handler script (see Card -> Script... in the menu) to reset the widgets, like so:

on view do
 c.show:"none"
 f1.text:0
 f2.text:0
end

But view[] isn't exclusively called when a card is first visited- it also activates any time you leave the editing tools (which may be fine), or when modal dialogs are dismissed. It's really intended more as a way to refresh the contents of the card than to initialize it.

You could tweak the above event handler to respond to the event "init" instead:

on init do
 c.show:"none"
 f1.text:0
 f2.text:0
end

Decker doesn't ever automatically call "init[]", but you can trigger it explicitly from a script. Let's say that you had a button on the first card of your deck that resets the game, and our init[] function and the widgets we wish to reset live on a card named "Bournemoth" (see Card -> Properties in the menu to set a card's name). The button's script could say:

on click do
 Bournemoth.event["init"]
end

Which would call the "init[]" event on the card Bournemoth (no matter where the original button resides) and the init[] handler on Bournemoth will reset it to an initial state long before our visit.

Does that explanation make sense?

There's only 150 (or more) to draw.

The main thing to keep in mind about tooltips- or anything activated on "hover"- is that it won't be accessible on devices that don't have a mouse, like most tablets.

That said, there are several ways we might approach implementing tooltips in Decker. The key will be the Pointer Interface, which we can consult to obtain the last-known coordinates of the user's pointing device.

Let's start with the simplest possible implementation: one button and one field containing a tooltip (named "tip"). We want the tooltip to appear when the mouse is over the button. By marking the button as "Animated", we can use its view[] event handler to control the visibility of the tooltip when pointer.pos is within the button's bounding box:

on view do
 tip.toggle["transparent"
  (min pointer.pos>me.pos)&(min pointer.pos<me.pos+me.size)
 ]
end


If you're doing this in many places, perhaps the above can be factored into a utility function that you move to a deck-level script:

on hover_caption target caption do
 caption.toggle["transparent"
  (min pointer.pos>target.pos)&(min pointer.pos<target.pos+target.size)
 ]
end

Simplifying the call sites to something like:

on view do
 hover_caption[me tip]
end

Instead of having individual animated widgets for each hover target, you could centralize the logic by having a single widget handling this responsibility for every button on a card; maybe even a locked invisible button with no other work to do. If you set up some kind of naming convention (like making the previous example be a button named "b1" and its tooltip named "b1_caption") you could write a generic routine which queried "card.widgets", found widgets with corresponding captions (or vice versa) and carried out the equivalent of the above automatically as buttons and captions were added and removed.

We could also build tooltip logic into a contraption, which would give us a place to keep around additional state to handle situations like an ease-in/ease-out animation for revealing the caption. Note that you'd probably design such a contraption to be layered underneath interactive targets like buttons.

Does that point you in the right direction?

Let's start with some context. All of the contraptions in WigglyKit share the convention of an "animation value"  consisting of a dictionary with the keys "frames" (a list of images) and "order" (a list of integers; indices into the list "frames"). There's nothing intrinsically special about this dictionary; it's just a convention to bundle together all the relevant information for a wiggly animation such that it can be easily moved around between those contraptions and manipulated with scripts.

WigglyKit offers an example of pulling the animation value out of a WigglyCanvas and then saving it as a GIF:

v:wc.value
# (optionally) make the background opaque:
v.frames:v.frames..copy[].map[0 dict 32]
gif.frames:v.frames @ v.order
gif.delays:10
write[gif]

As noted in the Decker reference manual for the built-in function write[] (see note 12), to save an animated GIF we need to call write[] with a dictionary with the keys "frames" (a list of images) and "delays" (a list of integers; how many 100ths of a second should each frame be displayed?)

So really, what we need to do to save a GIF outside the specific context of WigglyKit is make an appropriately-shaped dictionary and hand it to write[]:

gif.frames: ... # obtain or assemble a list of images, somehow
gif.delays: 10  # one number applies the delay to every frame
write[gif]

Obtaining/assembling that list of images will depend entirely upon your use case. We might grab them from a sequence of canvases:

gif.frames: (c1,c2,c3)..copy[]

We could do the above in a more verbose way, if we wanted:

gif.frames: c1.copy[],c2.copy[],c3.copy[]

Or perhaps we could use the recently-added ".images" attribute of rich-text fields to grab all the inline images stored in a field:

gif.frames: myRichTextField.images

Etc.

Does that help point you in the right direction?

Do you like car analogies for your UI components? Indulge yourself with

The RadioButton Contraption:


RadioButtons behave like checkboxes, but only one of the RadioButtons on a card with the same .group property may have a truthy .value. This contraption respects the standard .font, .show, and .locked attributes and fires a click[] event when clicked.

%%WGT0{"w":[{"name":"radio","type":"contraption","size":[134,18],"pos":[58,82],"font":"menu","show":"transparent","def":"radioButton","widgets":{"c":{"show":"transparent"},"l":{"font":"menu","show":"transparent","value":"First"},"b":{},"g":{"value":"a"}}}],"d":{"radioButton":{"name":"radioButton","size":[134,18],"resizable":1,"margin":[20,1,4,1],"description":"a selectable button which permits only a single selection within a group.","version":1,"script":"on get_label do l.text end\non set_label x do l.text:x view[] end\non get_group do g.text end\non set_group x do g.text:x end\non get_value do b.value end\non set_value_raw x do b.value:x view[] end\ndot:image[\"%%IMG0ABAADgAAAAAAAAAAB4APwA/AD8APwAeAAAAAAAAAAAA=\"]\noutline:image[\"%%IMG2ABAADgAVAQQACgECIAQBAgAHAQEgCAEBAAYBASAIAQEABQEBIAoBAQAEAQEgCgEBAAQBASAKAQEABAEBIAoBAQAFAQEgCAEBAAYBASAIAQEABwECIAQBAgAKAQQAFw==\"]\non view do\n l.font:       card.font\n l.show:c.show:card.show\n b.locked:card.locked\n if card.locked (outline,dot)..map[1 dict 13] end # gray out\n c.clear[]\n c.paste[outline]\n if b.value c.paste[dot 0,0 1] end\nend\non set_value x do\n gr:extract value where value..group=g.text from card.parent.widgets\n if x gr.  .value_raw:0\n else gr[0].value_raw:1\n end\n set_value_raw[x]\nend\non click do\n if !card.locked\n  set_value[1]\n  card.event.click\n end\nend","template":"on click do\n \nend","attributes":{"name":["label","group"],"label":["Label","Group"],"type":["string","string"]},"widgets":{"c":{"type":"canvas","size":[16,14],"pos":[1,2],"locked":1,"volatile":1,"border":0,"scale":1},"l":{"type":"field","size":[114,16],"pos":[19,1],"locked":1,"border":0,"style":"plain"},"b":{"type":"button","size":[134,18],"pos":[0,0],"style":"invisible"},"g":{"type":"field","size":[100,20],"pos":[149,-60],"locked":1,"show":"none"}}}}}
(1 edit)

There are two important steps:

1) Use "View -> Transparency Mask" mode to fill in opaque regions of your image. While Transparency Mask is active, the transparent parts of the card background image are shown as a mid-gray, and the "white" color in the palette is opaque white (pattern 32). In normal drawing mode, that element of the palette is transparent (pattern 0).

2) Set the Canvas widget you create to "Show Transparent".

It may be clearest to provide a demonstration:


Does that help?

It's not a bug; those tools are part of Decker, which is what WigglyPaint is made in. The deck is left "unlocked" so that you can modify WigglyPaint itself and if you wish and see how everything works.

I'm thrilled that you're having fun with Decker; using it for nonlinear supplementary material for a webcomic is a really cool idea!

Swiping generates "navigation gestures", which are the touchscreen-accessible equivalent to navigating the deck with the cursor keys. By default, Decker includes this handler for navigate[] events:

on navigate x do
 if x~"right" go["Next"] end
 if x~"left"  go["Prev"] end
end

If you want to disable this, edit the deck-level script (File -> Properties... -> Script...) and add an empty handler:

on navigate do end

Note that while in Widgets mode or using any drawing tools you'll still be able to navigate between cards with left/right cursor keys; overriding navigate[] disables this functionality while in Interact mode.

There are, of course, other things one could do by overriding navigate[], like setting up fancy 2d movement through the deck or even making keyboard-based puzzle games!

Does that make sense?

The "close" and "resize" buttons of this contraption don't do anything by default, but you can add your own behaviors with scripting.

If you open the properties panel for the MacWindow (double click on it while in Widgets mode) and click "Script..." you'll see the template script of the contraption:

on close do
end
on resize do
end

Fill in "on close ..." to make it hide the contraption:

on close do
 me.show:"none"
end

It's easy to un-hide the window again later in another script:

myMacWindow.show:"solid"

If you really want to destroy the contraption instance when you click the "close" button you can remove it from the deck:

on close do
 deck.remove[me]
end

Does that make sense?

Thanks for the info, NTTP! It would be great if this helps spread awareness of Decker.

Thanks for featuring Decker Fantasy Camp!

We got some really delightful submissions this time around, with lots of first-time participants.

Anything that you want to "remember" across event handlers needs to be stored in a widget. A true/false flag, for example, could be stored in a hidden checkbox. If we had a card named "flags" with a checkbox widget named "hasKey" your scripts might look something like

on click do
 flags.widgets.hasKey.value:1
 go[bednokey]
end
...
on click do
 if flags.widgets.hasKey.value
  go[bednokey "SlideUp"]
 else
  go[bed "SlideUp"]
 end
end

If the widget happens to be on the same card as the script which references it you can shorten paths like "flags.widgets.hasKey.value" to just "hasKey.value".

If you want to show or hide objects on a card, you may find it more flexible to use a transparent Canvas widget containing an image of the key. You can show, hide, or reposition canvases at will, sort of like a sprite object:

myCanvas.show:"solid"
myCanvas.show:"transparent" 
myCanvas.show:"none"
myCanvas.toggle["solid" booleanExpression]
myCanvas.pos:otherWidget.pos

This may also remove the need for a separate "flag" widget; the canvas itself will remember its visibility setting:

if bed.widgets.keyCanvas.show~"none"
 # ...
end

Does that help answer your question? There are lots of other examples of doing this sort of thing in this thread and elsewhere on the forum.