Kashi's Pixel Playground - Simple Paint-style App

Created a nice software using SpiderBasic ? Post you link here !
lanciamarcos
Posts: 4
Joined: Thu Nov 28, 2024 6:26 am

Kashi's Pixel Playground - Simple Paint-style App

Post by lanciamarcos »

Kashi’s Pixel Playground: A palette-driven point painter (SpiderBasic)

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
Image


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


Image
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)
Buttons mirror the shortcuts. Canvas auto-focuses so keys work after clicking UI.


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())