A tiny browser-based toy that paints with palette-cycled dots, random sets, and jitter.
The idea came from using some colored pencils which have multiple colors in each pencil, giving a kind of random color effect while drawing.
This project is named after my cat, Kashi.
Screenshot

Try it
https://www.friendlyskies.net/other-projects/kpp/

Modes & keys
- Default (D): 3-color cycling window (shift with 1/2)
- Random N (R): draws from N random palette colors (tweak N with 7/8)
- Jitter+Random (E): same as Random, but each dot jitters within a radius (tweak jitter with 3/4)
- Size: 5/6 (or mouse wheel) to change dot size
- Mouse: left-drag paints
- Utility: C clear • S export PNG (check your Downloads folder)
Notes
- Desktop-only, web-only for now (runs in browser via SpiderBasic)
- Early stages, rough edges expected
- Not sure if this belongs in “Showcase” yet since it’s simple… but it’s a release!
- Feedback welcome: coding, naming, palettes, fun mode ideas (dither brush? Voronoi splats? palette scrambler?)
Code: Select all
; SpiderBasic - Kashi's Pixel Playground
; Custom css background via inline JS
! const styleElement = document.createElement("style");
! styleElement.type = "text/css";
! styleElement.innerHTML = "html { background-color: purple; background-image:none; background-image: linear-gradient(to top, #3f51b1 0%, #5a55ae 13%, #7b5fac 25%, #8f6aae 38%, #a86aa4 50%, #cc6b8e 62%, #f18271 75%, #f3a469 87%, #f7c978 100%); }";
! document.head.appendChild(styleElement);
EnableExplicit
; ---------- IDs ----------
#WIN_MAIN = 0
#CANVAS = 1
#BTN_MODE_DEF = 10
#BTN_MODE_RND = 11
#BTN_MODE_JIT = 12
#BTN_SHIFT_L = 13
#BTN_SHIFT_R = 14
#BTN_N_DEC = 15
#BTN_N_INC = 16
#BTN_JIT_DEC = 17
#BTN_JIT_INC = 18
#BTN_SIZE_DEC = 19
#BTN_SIZE_INC = 20
#BTN_CLEAR = 21
#BTN_EXPORT = 22
#LBL_STATUS = 30
#LBL_HINTS = 31
; ---------- State ----------
Global bgColor.i = RGBA(255,255,255,255)
Global imgID.i = -1
Global isDrawing.i = #False
Global lastX.i, lastY.i
Global mode.i = 0 ; 0=default cycle, 1=random, 2=jitter
Global pointSize.i = 18
Global baseOffset.i = 0
Global defaultSpan.i = 3
Global rndN.i = 6
Global jitterRadius.i= 3
Global jitterInDefault.i = #False
Global drawTick.q = 0
; Palette
Global PalCount.i
Global Dim PAL.l(0)
Global Dim rndSet.l(0)
; ---------- Palette build ----------
Procedure AddCol(hex$)
Define r = Val("$" + Mid(hex$, 2, 2))
Define g = Val("$" + Mid(hex$, 4, 2))
Define b = Val("$" + Mid(hex$, 6, 2))
PAL(PalCount) = RGB(r,g,b)
PalCount + 1
ReDim PAL(PalCount)
EndProcedure
Procedure BuildPalette()
PalCount = 0 : ReDim PAL(0)
AddCol("#2e222f"):AddCol("#3e3546"):AddCol("#625565"):AddCol("#966c6c"):AddCol("#ab947a")
AddCol("#694f62"):AddCol("#7f708a"):AddCol("#9babb2"):AddCol("#c7dcd0"):AddCol("#ffffff")
AddCol("#6e2727"):AddCol("#b33831"):AddCol("#ea4f36"):AddCol("#f57d4a"):AddCol("#ae2334")
AddCol("#e83b3b"):AddCol("#fb6b1d"):AddCol("#f79617"):AddCol("#f9c22b")
AddCol("#7a3045"):AddCol("#9e4539"):AddCol("#cd683d"):AddCol("#e6904e"):AddCol("#fbb954")
AddCol("#4c3e24"):AddCol("#676633"):AddCol("#a2a947"):AddCol("#d5e04b"):AddCol("#fbff86")
AddCol("#165a4c"):AddCol("#239063"):AddCol("#1ebc73"):AddCol("#91db69"):AddCol("#cddf6c")
AddCol("#313638"):AddCol("#374e4a"):AddCol("#547e64"):AddCol("#92a984"):AddCol("#b2ba90")
AddCol("#0b5e65"):AddCol("#0b8a8f"):AddCol("#0eaf9b"):AddCol("#30e1b9"):AddCol("#8ff8e2")
AddCol("#323353"):AddCol("#484a77"):AddCol("#4d65b4"):AddCol("#4d9be6"):AddCol("#8fd3ff")
AddCol("#45293f"):AddCol("#6b3e75"):AddCol("#905ea9"):AddCol("#a884f3"):AddCol("#eaaded")
AddCol("#753c54"):AddCol("#a24b6f"):AddCol("#cf657f"):AddCol("#ed8099"):AddCol("#831c5d")
AddCol("#c32454"):AddCol("#f04f78"):AddCol("#f68181"):AddCol("#fca790"):AddCol("#fdcbb0")
If PalCount > 0 : ReDim PAL(PalCount-1) : EndIf
EndProcedure
; ---------- Utils ----------
Procedure.i Wrap(i.i, n.i)
If n <= 0 : ProcedureReturn 0 : EndIf
While i < 0 : i + n : Wend
While i >= n : i - n : Wend
ProcedureReturn i
EndProcedure
Procedure RebuildRandomSet()
Define i, j, pick, used
If rndN < 1 : rndN = 1 : EndIf
If rndN > PalCount : rndN = PalCount : EndIf
ReDim rndSet(rndN-1)
For i=0 To rndN-1
Repeat
pick = Random(PalCount-1)
used = #False
For j=0 To i-1
If rndSet(j) = pick : used = #True : Break : EndIf
Next
Until used = #False
rndSet(i) = pick
Next
EndProcedure
Procedure.s ModeName()
Protected s$ = "?"
Select mode
Case 0
s$ = "Default(3-cycle)"
If jitterInDefault
s$ + " +Jit"
EndIf
Case 1
s$ = "Random N"
Case 2
s$ = "Jitter+Random"
EndSelect
ProcedureReturn s$
EndProcedure
Procedure.s WindowPreview()
Protected i0 = Wrap(baseOffset + 0, PalCount)
Protected i1 = Wrap(baseOffset + 1, PalCount)
Protected i2 = Wrap(baseOffset + 2, PalCount)
ProcedureReturn "[" + Str(i0) + "," + Str(i1) + "," + Str(i2) + "]"
EndProcedure
Procedure UpdateStatus()
SetGadgetText(#LBL_STATUS, "Mode: " + ModeName() +
" Size: " + Str(pointSize) +
" N: " + Str(rndN) +
" Jit: " + Str(jitterRadius) +
" Offset: " + Str(baseOffset) +
" Win: " + WindowPreview())
EndProcedure
; ---------- Drawing ----------
Procedure RefreshCanvas(canvas)
If StartDrawing(CanvasOutput(canvas))
DrawingMode(#PB_2DDrawing_Default)
DrawImage(ImageID(imgID), 0, 0)
StopDrawing()
EndIf
EndProcedure
Procedure ClearAll()
If StartDrawing(ImageOutput(imgID))
Box(0,0,OutputWidth(),OutputHeight(), bgColor)
StopDrawing()
EndIf
RefreshCanvas(#CANVAS)
drawTick = 0
UpdateStatus()
EndProcedure
Procedure.i ChooseColor()
Select mode
Case 1, 2
ProcedureReturn PAL( rndSet(drawTick % rndN) )
Default
ProcedureReturn PAL( Wrap(baseOffset + (drawTick % defaultSpan), PalCount) )
EndSelect
EndProcedure
Procedure DrawDotAt(x.i, y.i)
Define color = ChooseColor()
Define jx = x, jy = y
If mode = 2 Or (mode = 0 And jitterInDefault)
jx = x + Random(jitterRadius*2) - jitterRadius
jy = y + Random(jitterRadius*2) - jitterRadius
EndIf
If StartDrawing(ImageOutput(imgID))
If pointSize <= 1
Plot(jx, jy, color)
Else
Box(jx - pointSize/2, jy - pointSize/2, pointSize, pointSize, color)
EndIf
StopDrawing()
EndIf
drawTick + 1
EndProcedure
; ---------- UI ----------
Procedure.i CreateUI()
Define w=1100, h=740, sidebar=280
If OpenWindow(#WIN_MAIN, 0, 0, w, h, "Kashi's Pixel Playground", #PB_Window_SystemMenu | #PB_Window_ScreenCentered) = 0
ProcedureReturn #False
EndIf
; Controls
ButtonGadget(#BTN_MODE_DEF, 16, 16, sidebar-32, 28, "Mode: Default (D)")
ButtonGadget(#BTN_MODE_RND, 16, 50, sidebar-32, 28, "Mode: Random (R)")
ButtonGadget(#BTN_MODE_JIT, 16, 84, sidebar-32, 28, "Mode: Jitter (E)")
ButtonGadget(#BTN_SHIFT_L, 16, 128, (sidebar-40)/2, 28, "Shift ◀ (1)")
ButtonGadget(#BTN_SHIFT_R, 24+(sidebar-40)/2, 128, (sidebar-40)/2, 28, "Shift ▶ (2)")
ButtonGadget(#BTN_N_DEC, 16, 168, (sidebar-40)/2, 28, "N– (7)")
ButtonGadget(#BTN_N_INC, 24+(sidebar-40)/2, 168, (sidebar-40)/2, 28, "N+ (8)")
ButtonGadget(#BTN_JIT_DEC, 16, 208, (sidebar-40)/2, 28, "Jit– (3)")
ButtonGadget(#BTN_JIT_INC, 24+(sidebar-40)/2, 208, (sidebar-40)/2, 28, "Jit+ (4)")
ButtonGadget(#BTN_SIZE_DEC, 16, 248, (sidebar-40)/2, 28, "Size– (5)")
ButtonGadget(#BTN_SIZE_INC, 24+(sidebar-40)/2, 248, (sidebar-40)/2, 28, "Size+ (6)")
ButtonGadget(#BTN_CLEAR, 16, 292, sidebar-32, 28, "Clear (C)")
ButtonGadget(#BTN_EXPORT, 16, 326, sidebar-32, 28, "Export PNG (S)")
TextGadget(#LBL_STATUS, 16, h-60, sidebar-32, 44, "Ready.")
TextGadget(#LBL_HINTS, 16, 420, sidebar-32, 200, "Keys: D/R/E, 1/2 shift, 7/8 N, 3/4 jitter, 5/6 size, C clear, S save")
CanvasGadget(#CANVAS, sidebar, 0, w - sidebar, h, #PB_Canvas_Keyboard)
; Offscreen buffer
imgID = CreateImage(#PB_Any, GadgetWidth(#CANVAS), GadgetHeight(#CANVAS), 32, bgColor)
If imgID = 0
MessageRequester("Failed to create drawing buffer image.")
End
EndIf
If StartDrawing(ImageOutput(imgID))
Box(0,0,OutputWidth(),OutputHeight(), bgColor)
StopDrawing()
EndIf
RefreshCanvas(#CANVAS)
; Make sure keyboard goes to canvas
SetActiveGadget(#CANVAS)
UpdateStatus()
ProcedureReturn #True
EndProcedure
; ---------- Handlers ----------
Procedure DoExport()
If imgID
Define fname$ = "palette_paint_" + FormatDate("%yyyy-%mm-%dd_%hh-%ii-%ss", Date()) + ".png"
ExportImage(imgID, fname$, #PB_ImagePlugin_PNG)
SetGadgetText(#LBL_STATUS, "Exported as " + fname$ + " — please check your Downloads folder.")
EndIf
SetActiveGadget(#CANVAS) ; keep keyboard focus on the canvas
EndProcedure
Procedure HandleButtons()
Select EventGadget()
Case #BTN_MODE_DEF
mode = 0
Case #BTN_MODE_RND
mode = 1 : RebuildRandomSet()
Case #BTN_MODE_JIT
mode = 2 : RebuildRandomSet()
Case #BTN_SHIFT_L
baseOffset = Wrap(baseOffset - 1, PalCount)
drawTick = 0
UpdateStatus()
Case #BTN_SHIFT_R
baseOffset = Wrap(baseOffset + 1, PalCount)
drawTick = 0
UpdateStatus()
Case #BTN_N_DEC
rndN - 1 : If rndN < 1 : rndN = 1 : EndIf : If mode > 0 : RebuildRandomSet() : EndIf
Case #BTN_N_INC
rndN + 1 : If rndN > PalCount : rndN = PalCount : EndIf : If mode > 0 : RebuildRandomSet() : EndIf
Case #BTN_JIT_DEC
jitterRadius - 1 : If jitterRadius < 0 : jitterRadius = 0 : EndIf
Case #BTN_JIT_INC
jitterRadius + 1 : If jitterRadius > 50 : jitterRadius = 50 : EndIf
Case #BTN_SIZE_DEC
pointSize - 1 : If pointSize < 1 : pointSize = 1 : EndIf
Case #BTN_SIZE_INC
pointSize + 1 : If pointSize > 64 : pointSize = 64 : EndIf
Case #BTN_CLEAR
ClearAll()
Case #BTN_EXPORT
DoExport()
EndSelect
UpdateStatus()
; return focus to canvas so keys continue working after clicking buttons
SetActiveGadget(#CANVAS)
EndProcedure
; Character-based key handling
Procedure HandleKey(k.i)
; normalize to uppercase ASCII when alphabetic
If k >= 'a' And k <= 'z' : k - 32 : EndIf
Select k
; Modes
Case 'D' : mode = 0 : UpdateStatus()
Case 'R' : mode = 1 : RebuildRandomSet() : UpdateStatus()
Case 'E' : mode = 2 : RebuildRandomSet() : UpdateStatus()
Case 'J'
jitterInDefault ! 1
UpdateStatus()
; Shift window
Case '1' : baseOffset = Wrap(baseOffset - 1, PalCount) : drawTick = 0 : UpdateStatus()
Case '2' : baseOffset = Wrap(baseOffset + 1, PalCount) : drawTick = 0 : UpdateStatus()
; Random N
Case '7' : rndN - 1 : If rndN < 1 : rndN = 1 : EndIf : If mode > 0 : RebuildRandomSet() : EndIf : UpdateStatus()
Case '8' : rndN + 1 : If rndN > PalCount : rndN = PalCount : EndIf : If mode > 0 : RebuildRandomSet() : EndIf : UpdateStatus()
; Jitter radius
Case '3' : jitterRadius - 1 : If jitterRadius < 0 : jitterRadius = 0 : EndIf : UpdateStatus()
Case '4' : jitterRadius + 1 : If jitterRadius > 50 : jitterRadius = 50 : EndIf : UpdateStatus()
; Size (also support + and - keys)
Case '5', '-' : pointSize - 1 : If pointSize < 1 : pointSize = 1 : EndIf : UpdateStatus()
Case '6', '+' : pointSize + 1 : If pointSize > 64 : pointSize = 64 : EndIf : UpdateStatus()
; Clear / Save
Case 'C' : ClearAll()
Case 'S' : DoExport()
EndSelect
EndProcedure
Procedure HandleCanvas()
Select EventType()
Case #PB_EventType_LeftButtonDown
isDrawing = #True
lastX = GetGadgetAttribute(#CANVAS, #PB_Canvas_MouseX)
lastY = GetGadgetAttribute(#CANVAS, #PB_Canvas_MouseY)
DrawDotAt(lastX, lastY)
RefreshCanvas(#CANVAS)
Case #PB_EventType_MouseMove
If isDrawing
Define mx = GetGadgetAttribute(#CANVAS, #PB_Canvas_MouseX)
Define my = GetGadgetAttribute(#CANVAS, #PB_Canvas_MouseY)
DrawDotAt(mx, my)
lastX = mx : lastY = my
RefreshCanvas(#CANVAS)
EndIf
Case #PB_EventType_LeftButtonUp
isDrawing = #False
Case #PB_EventType_MouseWheel
If GetGadgetAttribute(#CANVAS, #PB_Canvas_WheelDelta) > 0
pointSize + 1 : If pointSize > 64 : pointSize = 64 : EndIf
Else
pointSize - 1 : If pointSize < 1 : pointSize = 1 : EndIf
EndIf
UpdateStatus()
Case #PB_EventType_KeyDown
HandleKey(GetGadgetAttribute(#CANVAS, #PB_Canvas_Key))
EndSelect
EndProcedure
Procedure OnResize()
Define newW = GadgetWidth(#CANVAS)
Define newH = GadgetHeight(#CANVAS)
If newW <= 0 Or newH <= 0 : ProcedureReturn : EndIf
Define oldImg = imgID
imgID = CreateImage(#PB_Any, newW, newH, 32, bgColor)
If imgID = 0
imgID = oldImg : ProcedureReturn
EndIf
If StartDrawing(ImageOutput(imgID))
Box(0,0,newW,newH,bgColor)
If oldImg
DrawImage(ImageID(oldImg), 0, 0, newW, newH)
EndIf
StopDrawing()
EndIf
If oldImg : FreeImage(oldImg) : EndIf
RefreshCanvas(#CANVAS)
EndProcedure
Procedure OnClose()
End
EndProcedure
; ---------- Boot ----------
BuildPalette()
RebuildRandomSet()
If CreateUI() = #False : End : EndIf
; ---------- Bind ----------
BindEvent(#PB_Event_CloseWindow, @OnClose())
BindEvent(#PB_Event_SizeWindow, @OnResize())
BindGadgetEvent(#BTN_MODE_DEF, @HandleButtons())
BindGadgetEvent(#BTN_MODE_RND, @HandleButtons())
BindGadgetEvent(#BTN_MODE_JIT, @HandleButtons())
BindGadgetEvent(#BTN_SHIFT_L, @HandleButtons())
BindGadgetEvent(#BTN_SHIFT_R, @HandleButtons())
BindGadgetEvent(#BTN_N_DEC, @HandleButtons())
BindGadgetEvent(#BTN_N_INC, @HandleButtons())
BindGadgetEvent(#BTN_JIT_DEC, @HandleButtons())
BindGadgetEvent(#BTN_JIT_INC, @HandleButtons())
BindGadgetEvent(#BTN_SIZE_DEC, @HandleButtons())
BindGadgetEvent(#BTN_SIZE_INC, @HandleButtons())
BindGadgetEvent(#BTN_CLEAR, @HandleButtons())
BindGadgetEvent(#BTN_EXPORT, @HandleButtons())
BindGadgetEvent(#CANVAS, @HandleCanvas())