Skip to main content

Indie game storeFree gamesFun gamesHorror games
Game developmentAssetsComics
SalesBundles
Jobs
TagsGame Engines

Internet Janitor

924
Posts
58
Topics
7,955
Followers
27
Following
A member registered Aug 02, 2018 · View creator page →

Creator of

Recent community posts

It is possible to make these types of modifications, but it will require moderately involved scripting and familiarity with Decker's features. I strongly recommend starting by reading through All About Brushes, which explains how to extend and enhance Decker's native drawing tools. WigglyPaint takes advantage of these features for some of its wiggly drawing tools!

As of WigglyPaint v1.6, changing drawing tools is done via the changetool[] function in the Card-level script of the card "main". The PopOut contraption "pen" has a script that calls changetool[] like so:

on click do
 changetool[me me.name]
end

This in turn writes to the ".brush" attribute of "target", the main drawing area of WigglyPaint.

Drawing is directly carried out by the Wiggler prototype's prototype-level script: In Widgets mode, double-click the main drawing area to get the "Wiggler Properties" modal, then click "Prototype...", and then within the prototype editor choose "Prototype -> Script..." from the menu.

Within this script, we first see get_ and set_ functions used by WigglyPaint to configure the shape of the "highlighters", the eraser, the brush style, mirroring modes, and others. The draw[] function handles actually drawing lines and brush splats, and the drag[] function handles mirroring, playing appropriate brush sound effects, and doing some bookkeeping for the draw[] function.

Let's look at a simplified skeleton of the draw[] function:

on draw x o frames do
 if "eraser"~br.text
  # ...
 elseif "hi"~2 take br.text
  # ...
 elseif "pen"~br.text
  # ...
 elseif "pencil"~br.text
  # ...
 else
  # ...
 end
end

The arguments to this function are "x" (a position on the canvas), "o" (the previous location on the canvas in the current stroke), and "frames" (a list of three images comprising the wiggly animation), and the script also frequently references "br" (a field within the wiggler which tracks the name of the active brush) and "c" (the canvas widget that accounts for the visual appearance of the wiggler and is also used as a temporary drawing surface for brushstrokes). Based on the brush selected, we use the drawing coordinates we're supplied to update the frames of the animation!

The simplest tool is that last "default" option:

c.brush:br.text
each f in frames
  c.paste[f]
  c.line[wiggle[o] wiggle[x]]
  f.paste[c.copy[]]
end

All this does is choose a custom Decker brush by name and then draw lines on each frame, randomly offsetting the start and end coordinates each time. This means that without modifying the wiggler at all, we could use the information in All About Brushes to define a new brush, and then make a button or popout widget which selects that tool by name with changetool[].

The built-in custom brushes WigglyPaint uses are defined in a module called "pens". If you want to see and edit their innards, presently the easiest way is to save wigglypaint as a .deck or .html file, open it in a text editor and search for "{module:pens}". Here's one of the functional brushes you'll find inside:

# 4x4 stipples:
t:image@"\n"split 1 drop"
%%IMG0AAQABAAwMAA=
%%IMG0AAQABMDAAAA=
%%IMG0AAQABAAAYGA=
%%IMG0AAQABADAwAA=
%%IMG0AAQABIAAAIA=
%%IMG0AAQABAAgAEA=
%%IMG0AAQABEAAEAA=
%%IMG0AAQABAAAgBA="
brush[on StippleTiny do random[t] end]

This corresponds to the PopOut contraption in WigglyPaint which is also named "StippleTiny". It selects from a small set of pixel masks every time it "splats" itself along a line. Copying and pasting these "%%IMG..." strings into Decker (without quotes!) will let you see and edit the masks. It's a little tedious, but not as scary as it might initially seem!

The "highlighters" use the end of their names to specify one of Decker's pattern indices to draw in. Their implementation is more complex than a generic drawing tool because they work by compositing their brush shape onto the wiggler's canvas (c) while preserving "black" lines (pattern 1). Here "sh" is another internal field which stores an encoded brush shape image that we need to center at the drawing coordinates:

pat:0+2 drop br.text
img:image[sh.text]
s:img.size
y:x-s/2
each f in frames
 w:wiggle[y]
 f.paste[img.copy[].merge[f.copy[w s].map[1 dict 1 pat]] w 1]
end

If you didn't want to leave black lines alone, this could become simpler; we just need to map pattern 0 of the brush shape to our highlighter's pattern index, and everything else to pattern 0 (transparent) up-front:

pat:0+2 drop br.text
img:image[sh.text].map[0 dict pat 0]
s:img.size
y:x-s/2
each f in frames
 f.paste[img wiggle[y] 1]
end

Wiring this change up to a UI in wigglypaint- a checkbox, togglebutton, etc- is left as an exercise to the reader; you can reference the surrounding code and widgets for how all the other properties of the wiggler work.

Does any of that help?

If you're looking for a quick and easy fix on a large deck that has 16 pixels of unwanted empty space above every card, you could use The Listener to fill the rectangle of every card covered by the menu with solid black like so:

each c in deck.cards
 c.image.paste[image[c.size[0],16].map[0 dict 1]]
end

The Listener is a very powerful tool for doing things that would otherwise be extremely tedious and repetitive.

I  strongly recommend experimenting with this sort of thing in a backup copy of your deck to ensure you don't accidentally lose work!

(1 edit)

Have you copied the "col" module from the "All About Color" deck into the deck you're making using the Font/DA Mover?

Are you using the latest release of Decker (v1.66) and the "col" module (v1.1)? (As of this writing.)

Can you provide us with the Lil script you're trying to use? I can't correct problems if I don't know what you're trying.

You're very welcome. Knowing that my work helps real humans express themselves creatively is what keeps me building.

I recently implemented a module for working with "Hershey Fonts", a very simple format for representing vector-based text with straight line segments:

This module can parse and format the ".jhf" font format, lay out and draw multiline text using these fonts, and I've also included a variety of examples demonstrating how to post-process and animate vextor text paths. I intend to include a version of this library with the next Decker release, but it doesn't require any bleeding-edge features to use right now:

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

Currently the included fonts only support the ASCII character set directly (though Hershey fonts do exist for Greek, Japanese, and several other sets of specialized symbols). If there's interest, I think it would be valuable to try extending support for more (or all) of the DeckRoman character set.

Questions? Thoughts?

This looks fantastic, Screwtapello- thanks for building it and sharing with the community!

(1 edit)

That's a rather expansive question!

Exporting GIF images with write[] and asking users to convert them to other image formats as desired will generally be the simplest approach. Decker natively supports GIF because it's a simple and universally-supported format that is also a good match for Decker's paletted-color model.

If you decide to dig deeper, the image interface has a .pixels attribute which, when read, will give you the pixel values of that image as a list of lists of numbers (a matrix).

These numbers will be Decker pattern indices, so you'd need to do some work to "flatten out" 1-bit patterns and animated patterns (if applicable) and then look up the corresponding RGB colors from Decker's active palette. The internals of the PDF module might be a useful reference.

In principle, you could use an Array interface to construct your own PNG encoder in pure Lil, but this could be a significant amount of work! The CUR format is a bit simpler than PNG and (so far as I'm aware) builds on BMP, which is also relatively straightforward. An encoder is, at least, quite a bit simpler than a decoder, since you can avoid implementing all the features and options you don't need for your intended application.

If you only care about PNG export from web builds of your tools, you could also consider writing a module that uses the danger zone to call some JavaScript from Lil and use ordinary web APIs to perform the conversion. See The Forbidden Library for examples of doing this kind of JS/Lil interop.

Does any of that point you in the right direction?

This is truly a delight. I learned a lot about our tiny, chitinous friends.

If there's anything you'd like help with, during or after your jam deadline, please feel free to reach out on the Decker Community forum!

(1 edit)

For my own mysterious reasons, this afternoon I wrote a simple recursive descent parser for the OpenStep ASCII .plist format, an ancient text-based data serialization format which loosely resembles- but is not- JSON.

While it is exceedingly unlikely that more than one of you folks will have a use for this specific parser, it may serve as a useful example of how to write parsers in Lil.

The high-level idea is to create a closure with a local variable tracking our progress at consuming the input string, and then build a few utility functions for trimming whitespace, consuming tokens defined by parse patterns, and matching-and-consuming special leading characters. We can then write a recursive procedure in terms of those helpers which identifies .plist dictionaries, lists, byte arrays, and strings, and converts them into their equivalent Lil representations:

on parse_ascii_plist str do
    local t:trim str
    on ltrim x do ("%*.2r \n%n" parse x)drop x end
    on tok x do local d:x parse t   t:ltrim[(last d)drop t]   first d end
    on at x do if x~first t t:ltrim[1 drop t]   1 end end
    on rec do
        if at["{"] # dicts
            local r:keystore[]
            local f:1 while f
                if at["}"]
                    f:0
                else
                    local k:rec[] at["="]
                    r[k]:rec[]    at[";"]
                end
            end r.dict
        elseif at["("] # lists
            local r:()
            local f:1 while f
                if at[")"]
                    f:0
                else
                    r:r,list rec[] at[","]
                end
            end r
        elseif "<"~first t # arrays
            local d:tok["<%.24r0123456789abcdefABCDEF \n>%n"]
            array[].cat[(list "%h") parse 2 window "" fuse ("\n"," ") drop d]
        elseif "\""~first t # quoted string
            # note: this mangles \U escapes i've seen in some real-world plists,
            # but gets us the usual json-style escape sequences for free:
            tok["%j%n"]
        else # non-quoted string
            # note: in practice these leaves may be (signed!) numbers;
            # consider identifying and parsing them separately:
            tok["%.63rabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-%n"]
        end
    end
    rec[]
end

For the sake of speed and simplicity I make the (potentially unsafe!) assumption that the .plists are syntactically well-formed and don't contain any consequential non-DeckRoman characters.

If you encounter any .plist files in the wild that this can't sufficiently handle, I'd be happy to make further refinements. Happy data-munging. :)

Yep! Most of the methods of an Image modify the image in place and return the original image, so you can chain calls like this: .map[], .transform[], .rotate[], .translate[], .scale[], .outline[], .merge[], and .paste[]. (image.copy[] returns the region you're copying as a new image!)

The same applies to most of the drawing methods of a Canvas widget: .clip[], .clear[], .rect[], .invert[], .box[], .fill[], .line[], .poly[], .text[], .segment[], .outline[], .merge[], and .paste[]

I tried reproducing the scenario you describe, but it seems to work as you intended, without showing any odd sizing for the lower dialog box.

Here is a card with my reproduction that you can paste into a the Dialogizer deck to compare to your own:

%%CRD0{"c":{"name":"card1","script":"s_lower:()  \ns_lower.pos:\"dialog_lower\" \ns_lower.size:300,0\ns_upper:()  \ns_upper.pos:\"dialog_upper\"\ns_upper.size:200,0\n\non command x do   \n if x~\"lower\"   \n  dd.style[s_lower]   \n elseif x~\"upper\"   \n  dd.style[s_upper]    \n end \nend","widgets":{"button1":{"type":"button","size":[60,20],"pos":[23,221],"script":"on click do\n dialog_drip.view:\"none\"   \n dd.open[deck]   \n dd.say[text_a.value]   \n dd.close[]\nend","text":"Do It"},"text_a":{"type":"field","size":[150,116],"pos":[23,92],"value":"!upper\n\nthe upper text but long enough to demonstrate that it's wrapping and so on and so forth, blah blah blah\n\n!lower\n\nthe lower text. again, this needs to be appropriately long to demonstrate text wrapping."},"dialog_lower":{"type":"button","size":[60,20],"pos":[133,294]},"dialog_upper":{"type":"button","size":[60,20],"pos":[236,43]}}},"d":{}}

Please make sure you're using Decker v1.66 (see Decker -> About in the menu) and Dialogizer v1.8 (see File -> Resources in the menu and select the "dd" module); using an outdated version of Decker with a modern Dialogizer or vice versa might cause strange behavior?

Patterns in Decker are "logical colors". Decker images can retain the appearance of 1-bit stippled textures across movements and fills because every pixel knows what pattern it belongs to. There's some information you may find useful in All About Color. Image formats like PNG, BMP, and GIF don't understand Decker's model of patterns, so round-tripping images through those formats loses information. Decker uses a text-based image encoding to store its images in the clipboard and in ".deck" files.

With scripting, you can write tools within Decker that import images in a 16-color or 256-gray palette and then re-map those to different colors or patterns. This would make it possible to draw images in an external tool using an appropriate palette to control which regions of your image become specific Decker patterns. Likewise, it's possible to write scripts to export images with an arbitrary 256-color palette, and you could use this to save patterns with solid colors another image editor can understand.

If there are specific things you find difficult to do within Decker, we might be able to offer additional advice. If you haven't seen it already, I highly recommend reading through Phinxel's Phield Notes for an overview of Decker's capabilities.

Horrendous. I am very strongly opposed to "genai" and nothing could be more disrespectful to my work than using it to shill slop.

Shhh, transparency mask mode on the options screen is a secret to everybody!

Nobody will know it gives you the mask marker so you can draw opaque backgrounds for semi-transparent images, suitable for stickers in chat apps or sprites in games, as long as we keep quiet about it...

Whatever you do, don't clue anyone into this hidden feature, or any of the other hidden goodies and easter eggs lurking inside WigglyPaint!

Presuming a slider named "slider1", you could give a button a script like

on click do
 slider1.value:slider1.value + 1
end

This situation is very similar to one of the examples in The Decker Guided Tour and one of the examples in the introductory primer in the Lil Reference Manual.

(1 edit)

Presuming the canvases are on the same card and have a naming convention like "c1", "c2", "c3", "c4" and the slider is set to an integer range between 0 and 3 you could write something like

on change val do
 canvases:c1,c2,c3,c4
 X.paste[canvases[val].copy[]]
end

"#" is not a valid Lil identifier; it is used in Lil scripts to indicate a comment, which ignores the remainder of the line. Lil identifiers are described in the Lil Reference Manual as follows:

Variable and function names may contain any alphanumeric characters (as well as ? and _), but must not start with a digit. 

If you give a widget a name which is not a valid identifier, it will not be automatically available as a local variable, but it can be accessed by name from the card's ".widgets" dictionary:

card.widgets["#"]

Your description of what you're trying to do is inconsistent and unclear. If the idea is to- for example- index into images stored in a rich-text field named Y and paste the image corresponding to the value of the slider ("me") onto a canvas named X, you could use something like:

on change val do
 X.paste[Y.images[val]]
end

I have revised the behavior of read[] in Decker to return nil if a user cancels the open file dialog instead of a hint-appropriate empty value. This makes it possible to distinguish explicit cancels from selecting and reading empty files, and makes it easier to use 'unless' to substitute in a default if needed:

https://github.com/JohnEarnest/Decker/commit/182398aed65b6ce900592137ebcba22a4c304b5e

In practice, I don't expect this to be a significant breaking change, since most of the examples in the wild that I have reviewed either explicitly check the size of the result (e.g. 'if count read[] ...'), gracefully tolerate a nil as an acceptable empty value (coercing it to a string, dict, etc), or make no attempt whatsoever to handle even an empty result.

Note that this change does not alter the existing behavior of 'danger.read[path hint]' or lilt's 'read[path hint]', which produce empty values upon a read failure, such as lacking filesystem permissions.

I highly encourage anyone with an idea for new documentation, references, learning materials, or tutorials to take a stab at making them and share them with the community.

I can't suit every need myself, and presenting information with more voices and for more learning styles is always better!

Added those shortcuts: https://github.com/JohnEarnest/Decker/commit/c9d1b8076db54b857c57860e3440a08221b291c1

Thanks for the suggestions.

  • The script editor has "Go to Deck" / "Go to Card" menu items which don't presently have a keyboard shortcut; adding shortcuts would be very simple and at least get you a two-keystroke path to what you want. Any preferences?
  • I'll give a file-filtering hint for read[] some thought, but it might be tricky to make portable; Browsers (notionally) want filters based on MIME types- which often don't exist or aren't well-recognized for binary formats, and at best this information is a vague suggestion that they may ignore entirely. I've observed very inconsistent behavior for both MIME and extension-based filters, especially on mobile browsers.
  • I don't see a way of supplying filenames as a result from read[] without breaking its contract, and in turn many existing decks. It also poses some potential portability issues; on mobile browsers, for example, importing an image may allow a user to take a photo from their phone's camera instead of choosing an image from the local filesystem. I can see how this could be useful, but I'm hesitant to make the breaking change.
  • Returning an empty array on canceling a read[] of a binary file is consistent with returning an empty image on canceling a read[] of an image. This design hasn't been re-examined since the introduction of nil as a new option for in-band signaling, though. I'll give this some thought, too.

Given an integer id "tid" and a grid named "tasks" with numeric columns "id" and "state", you should be able to toggle a state cell like so:

tasks.value:update state:!state where id=tid from tasks.value

If you're seeing odd behavior, I'd recommend confirming that the "state" and "id" columns of your grid actually contain numbers, and not strings; you can see this distinction more easily if you inspect the table via the listener; strings in cells will be enclosed in double-quotes:


If you set the grid's format string appropriately, (in the above example "isi" for an integer column, a string column, and an integer column) you can "re-normalize" the data in a grid by toggling between JSON and CSV editing mode in the grid's properties dialog.

It may be a little surprising that negating the string "0" with ! produces the number 0, instead of 1, but this is because ! is not an arithmetic operator, it is a logical one, and all non-empty strings are considered "truthy" in Lil:

!(nil,"","0","1",0,1)
# (1,1,0,0,1,0)

If you're ever wanting the logical negation of a grid column that may contain "0"/"1" strings, you could also resolve the issue by adding zero to the string first, coercing it to a number:

0+(nil,"","0","1",0,1)
# (0,0,0,1,0,1)
!0+(nil,"","0","1",0,1)
# (1,1,1,0,1,0)

Or, in this context,

tasks.value:update state:!0+state where id=tid from tasks.value

Does that help?

I think that intercepting go[] and migrating a timer between cards as the user moves through the deck sounds a bit brittle.

Perhaps the technique described here would be helpful:

https://itch.io/t/5630988/shared-state-and-singleton-widgets

You could place an instance of such a contraption- with an internal animated widget- on every card where time is meant to be passing.

WigglyPaint can't do that out of the box, but it's possible with some minor tweaks!

If you switch to the "Widgets" tool and double-click on the invisible button over the "Export" floppy disk in the bottom left corner (it is currently named "button1"), you can click the "Script..." button in its properties panel to view the script that handles GIF exports. Currently it should look something like this:

on click do
 play["snap"]
 t:if options.widgets.mask.value () else 0 dict 32 end
 gif.frames:target.frames..copy[].map[t]
 gif.delays:3 take 10
 write[gif]
end

Replace it with the following:

on click do
 play["snap"]
 t:if options.widgets.mask.value () else 0 dict 32 end
 o:image[card.size]
 each w in (target.index+1) drop card.widgets
  o.paste[app.render[w] w.pos]
 end
 o:o.copy[target.pos target.size]
 gif.frames:target.frames..copy[].map[t].paste[o 0,0 1]
 gif.delays:3 take 10
 write[gif]
end

This updated script finds all the widgets on the card which are above the WigglyPaint drawing canvas (it is currently named "target"), draws them without the card background, and then overlays that drawing on each frame of the GIF.

When you make customizations like this I recommend saving a local copy of WigglyPaint so you don't have to perform this modification every time. Everything about Wigglypaint can be improved to suit your preferences!

You can change a field widget's font by selecting it with the "Widgets" tool active and choosing "Font..." from the main "Widgets" menu. By default, WigglyPaint only includes Decker's three built-in fonts, but you can install more: see the "All About Fonts" deck which comes in Decker's "examples" directory for more fonts, more information about fonts, and a font editor. You can also download a copy from here:

https://beyondloom.com/decker/fonts.html

Does that work for you?

You could modify this contraption example to allow external events to reset the timer, but you really don't need a contraption at all to keep track of cooldowns; just an animated widget and some scripting. I recommend reading the sections of Phinxel's Phield Guide on Timing and Animated Widgets, as well as viewing the prototype script of the above example.

Well, you can apply the "join" operator to a pair of lists to zip them together into tuples:

t.page join t.id
# ((1,1),(1,2),(2,1))

You can use the "in" operator to search for a tuple, but you'll have to enlist the tuple and then unpack the result from a count-1 list to avoid searching for individual components of the tuple in the zipped list:

(1,2) in t.page join t.id
# (0,0)
first (list 1,2) in t.page join t.id
# 1
first (list 1,3) in t.page join t.id
# 0

If I understand correctly, you should be able to do this more straightforwardly with

if grid.value[idx]
 # ...
end

Indexing a table with a number fetches the corresponding row of the table as a dictionary, which will be "truthy" so long as the table has at least one column. If the row does not exist, indexing in this manner will result in nil, which is a "falsey" value:

t:insert a b with 11 22 33 44 55 66 end 
# +----+----+
# | a  | b  |
# +----+----+
# | 11 | 22 |
# | 33 | 44 |
# | 55 | 66 |
# +----+----+
t[2]
# {"a":55,"b":66}
t[-1]~nil
# 1
t[3]~nil
# 1

Another approach would be to use the "count" operator to determine the number of rows in the table, and compare that to "idx":

count t
# 3

Does that help at all?

Lil doesn't have a keyword for performing an early return from a function, or otherwise an equivalent of break/continue for each/while loops or a throw/catch exception mechanism. All control flow is strictly structured and local.

For some situations where you might want to guard a clause, you can take advantage of the fact that if ... elseif ... else ... end structures, like all control structures in Lil, are expressions which return their result:

C:

int foo(){
 if(cond1){return a;}
 if(cond2){foo();return b;}
 bar();quux();return c;
}

Lil:

on foo do
 if cond1 a
 elseif cond2 foo[] b
 else bar[] quux[] c
 end
end

I encourage breaking functions down into fairly short definitions. You can nest function declarations within one another if you need "helper" functions that are only used in one place, and those nested functions will close over the lexical scope of their "parent" function.

You'll need to upgrade your decks to the latest version of "twee", too, and possibly do the same for other modules.

I recommend reviewing the release notes for the intervening releases, which include detailed information about any breaking changes and mitigation steps.

You'll need to set the contraption instance itself to "Show Transparent".

You can do this manually, via the menu ("Widgets -> Show Transparent") or programmatically, by giving the contraption an internal script which initializes this attribute in response to the view[] event:

on view do
 card.show:"transparent"
end

Just as a followup, Decker 1.66 makes this technique a tiny bit more powerful:

By altering app.cursor it's possible for blocking scripts to override the appearance of a mouse cursor. Setting

app.cursor:"point"

While the mouse is over a clickable "button" (real or simulated) will improve the illusion that there's a real widget there.

The new gamepad interface opens up additional input options, too: the "left"/"right" directional buttons and the "cancel" button aren't used by Dialogizer, so any of them could be used to exit a game, mute sound effects, summon a menu, etc, via the keyboard or a physical game controller.

I spent some more time looking into gamepad support and found my information was inaccurate- or at least out of date! Browser gamepad APIs appear to work outside a "secure context" in most browsers today.

Decker 1.66 now includes a "gamepad" interface which abstracts over a subset of keyboard and physical gamepad input, and Dialogizer has been updated to support this feature. On a keyboard you can now navigate dialog boxes with space+arrow keys.

When the distinction between a number and a string matters, the simplest way to convert the string into a number is with an arithmetic identity operation, like adding zero, as in your last example.

The format operator is for converting data into strings (or lists of strings); its counterpart for decomposing strings into other values is "parse":

 "%i" parse "123"
123
 "%i|%i|%i" parse "123|34|89"
(123,34,89)
(2 edits)

Just at a glance, it seems like the latest version of Decker will meaningfully reduce the size of the deck, and thus improve loading time. (Don't forget to update all the modules you're using along with Decker itself!)

I have some theories about incidental performance overhead caused by the large number of cards in this deck that I might be able to fix- or at least improve- in the next Decker release; I'll investigate.

EDIT: I've identified and addressed an internal bottleneck; having 500+ cards was causing event dispatch to take longer than usual, exacerbated by the fact that Dialogizer emits animate[] events at 60hz. Decker now handles binding local card and widget names for event scripts more efficiently.  I think this tweak should significantly improve the performance of your deck in both web- and native-Decker. These improvements are available now if you build Decker from source and will be included in the v1.66 release, probably available on Friday.

(1 edit)

Congratulations, L'alpha Lara!

You're very welcome to share links to your Decker projects in this forum. (That goes for everyone!)

I do second Millie's advice to upgrade Decker to the latest version. Skimming the release notes for intervening versions is a good idea; sometimes new revisions will require tweaks to old projects for compatibility reasons. If you're using any modules or contraptions in your project, you may need to upgrade those to their latest releases as well.

In general Web-Decker is somewhat slower than Native-Decker, especially when running on older computers. If you can be more specific about what you mean by "lagging" we might have some suggestions.

Houdini Magazine's Decker App Jam is all about making tools and utilities with Decker. Check it out!

It's very frustrating; the predatory knockoffs have aggressive SEO, so people casually googling for "wigglypaint" tend to find them first, and the users mislead in this way share them with others, reinforcing the problem. It hurts my heart to see artists exploited and charged money for an outdated copy of a tool that should be free. :(

Different browsers have inconsistent behavior regarding file:// and localhost-served lone pages; in many cases a user would need a local webserver and a self-signed ssl certificate. It's a very frustrating foot-gun that's especially difficult and confusing for people who aren't already steeped in web development.

When a row or cell of a grid widget is clicked, it is sent a click[] event with the active row as an argument. See Events in the Decker reference manual.

on click row do
 if row~0
  alert["you clicked the first one"]
 elseif row~1
  alert["you clicked the second one"]
 else
  alert["you clicked something else"]
 end
end