Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 89-apps-should-be-able-to-specify-if-they-want-wifi-to-be-disabled-when-entering-them
  • 9Rmain
  • allow-reloading-sunmenu
  • always-have-a-wifi-instance
  • anon/gpndemo
  • anon/update-sim
  • anon/webflasher
  • app_text_viewer
  • audio_input
  • audio_io
  • blm_dev_chan
  • ch3/bl00mbox_docs
  • ci-1690580595
  • dev_p4
  • dev_p4-iggy
  • dev_p4-iggy-rebased
  • dx/dldldld
  • dx/fb-save-restore
  • dx/hint-hint
  • dx/jacksense-headset-mic-only
  • events
  • fil3s-limit-filesize
  • fil3s-media
  • fpletz/flake
  • gr33nhouse-improvements
  • history-rewrite
  • icon-flower
  • iggy/stemming
  • iggy/stemming_merge
  • led_fix_fix
  • main
  • main+schneider
  • media_has_video_has_audio
  • micropython_api
  • mixer2
  • moon2_demo_temp
  • moon2_migrate_apps
  • more-accurate-battery
  • pippin/ctx_sprite_sheet_support
  • pippin/display-python-errors-on-display
  • pippin/make_empty_drawlists_skip_render_and_blit
  • pippin/more-accurate-battery
  • pippin/tcp_redirect_hack
  • pippin/tune_ctx_config_update_from_upstream
  • pippin/uhm_flash_access_bust
  • pressable_bugfix
  • py_only_update_fps_overlay_when_changing
  • q3k/doom-poc
  • q3k/render-to-texture
  • rahix/flow3rseeds
  • raw_captouch_new
  • raw_captouch_old
  • release/1.0.0
  • release/1.1.0
  • release/1.1.1
  • release/1.2.0
  • release/1.3.0
  • release/1.4.0
  • restore_blit
  • return_of_melodic_demo
  • rev4_micropython
  • schneider/application-remove-name
  • schneider/bhi581
  • schneider/factory_test
  • schneider/recovery
  • scope_hack
  • sdkconfig-spiram-tinyusb
  • sec/auto-nick
  • sec/blinky
  • sector_size_512
  • shoegaze-fps
  • smaller_gradient_lut
  • store_delta_ms_and_ins_as_class_members
  • task_cleanup
  • uctx-wip
  • w1f1-in-sim
  • widgets_draw
  • wifi-json-error-handling
  • wip-docs
  • wip-tinyusb
  • v1.0.0
  • v1.0.0+rc1
  • v1.0.0+rc2
  • v1.0.0+rc3
  • v1.0.0+rc4
  • v1.0.0+rc5
  • v1.0.0+rc6
  • v1.1.0
  • v1.1.0+rc1
  • v1.1.1
  • v1.2.0
  • v1.2.0+rc1
  • v1.3.0
  • v1.4.0
94 results

Target

Select target project
  • flow3r/flow3r-firmware
  • Vespasian/flow3r-firmware
  • alxndr42/flow3r-firmware
  • pl/flow3r-firmware
  • Kari/flow3r-firmware
  • raimue/flow3r-firmware
  • grandchild/flow3r-firmware
  • mu5tach3/flow3r-firmware
  • Nervengift/flow3r-firmware
  • arachnist/flow3r-firmware
  • TheNewCivilian/flow3r-firmware
  • alibi/flow3r-firmware
  • manuel_v/flow3r-firmware
  • xeniter/flow3r-firmware
  • maxbachmann/flow3r-firmware
  • yGifoom/flow3r-firmware
  • istobic/flow3r-firmware
  • EiNSTeiN_/flow3r-firmware
  • gnudalf/flow3r-firmware
  • 999eagle/flow3r-firmware
  • toerb/flow3r-firmware
  • pandark/flow3r-firmware
  • teal/flow3r-firmware
  • x42/flow3r-firmware
  • alufers/flow3r-firmware
  • dos/flow3r-firmware
  • yrlf/flow3r-firmware
  • LuKaRo/flow3r-firmware
  • ThomasElRubio/flow3r-firmware
  • ai/flow3r-firmware
  • T_X/flow3r-firmware
  • highTower/flow3r-firmware
  • beanieboi/flow3r-firmware
  • Woazboat/flow3r-firmware
  • gooniesbro/flow3r-firmware
  • marvino/flow3r-firmware
  • kressnerd/flow3r-firmware
  • quazgar/flow3r-firmware
  • aoid/flow3r-firmware
  • jkj/flow3r-firmware
  • naomi/flow3r-firmware
41 results
Select Git revision
  • 9Rmain
  • anon/gpndemo
  • anon/update-sim
  • anon/webflasher
  • audio_input
  • audio_io
  • bl00mbox
  • bl00mbox_old
  • captouch-threshold
  • ch3/bl00mbox_docs
  • ci-1690580595
  • compressor
  • dev_p4
  • dev_p4-iggy
  • dev_p4-iggy-rebased
  • dos
  • dos-main-patch-50543
  • events
  • fm_fix
  • fm_fix2
  • fpletz/flake
  • history-rewrite
  • icon-flower
  • iggy/stemming
  • iggy/stemming_merge
  • json-error
  • main
  • main+schneider
  • media-buf
  • micropython_api
  • moon2_applications
  • moon2_demo_temp
  • moon2_gay_drums
  • passthrough
  • phhw
  • pippin/display-python-errors-on-display
  • pippin/make_empty_drawlists_skip_render_and_blit
  • pippin/media_framework
  • pippin/uhm_flash_access_bust
  • pressable_bugfix
  • q3k/doom-poc
  • rahix/big-flow3r
  • rahix/flow3rseeds
  • raw_captouch_new
  • raw_captouch_old
  • release/1.0.0
  • release/1.1.0
  • release/1.1.1
  • rev4_micropython
  • schneider/application-remove-name
  • schneider/bhi581
  • schneider/factory_test
  • schneider/recovery
  • scope
  • scope_hack
  • sdkconfig-spiram-tinyusb
  • sec/auto-nick
  • sec/blinky
  • simtest
  • slewtest
  • t
  • test
  • test2
  • uctx-wip
  • view-think
  • vm-pending
  • vsync
  • wave
  • wip-docs
  • wip-tinyusb
  • v1.0.0
  • v1.0.0+rc1
  • v1.0.0+rc2
  • v1.0.0+rc3
  • v1.0.0+rc4
  • v1.0.0+rc5
  • v1.0.0+rc6
  • v1.1.0
  • v1.1.0+rc1
  • v1.1.1
  • v1.2.0
  • v1.2.0+rc1
  • v1.3.0
83 results
Show changes
Showing
with 1192 additions and 323 deletions
[app]
name = "Clouds"
menu = "Apps"
[entry]
class = "Clouds"
category = "Badge"
[metadata]
author = "Flow3r Badge Authors"
......
from .main import CapTouchDemo
from st3m.application import Application
from st3m.goose import List
from st3m.input import InputState
from st3m.ui.view import ViewManager
from ctx import Context
App = CapTouchDemo
import captouch
import cmath
import math
class Dot:
size = None
pos = 0j
filled = True
def draw(self, ctx: Context):
if self.size is None or self.pos is None or self.filled is None:
return
ctx.save()
ctx.translate(self.pos.real, self.pos.imag)
ctx.rotate(cmath.phase(self.pos))
ctx.move_to(-self.size / 2, -self.size / 2)
ctx.rel_line_to(self.size, self.size / 2)
ctx.rel_line_to(-self.size, self.size / 2)
ctx.close_path()
ctx.fill() if self.filled else ctx.stroke()
ctx.restore()
class CapTouchDemo(Application):
def on_enter(self, vm: ViewManager):
super().on_enter(vm)
self.dots: List[Dot] = [Dot() for x in range(10)]
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
for i in range(10):
petal = ins.captouch.petals[i]
dot = self.dots[i]
pos = 0j if petal.pos is None else petal.pos * 30
dot.pos = (pos + 70) * captouch.PETAL_ROTORS[i]
dot.size = 5 + 8 * math.sqrt(petal.raw_cap)
dot.filled = not petal.pressed
def draw(self, ctx: Context) -> None:
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
for i in range(10):
ctx.rgb(*((0.0, 0.8, 0.8) if i % 2 else (1.0, 0.0, 1.0)))
self.dots[i].draw(ctx)
if __name__ == "__main__":
import st3m.run
st3m.run.run_app(CapTouchDemo)
[app]
name = "Captouch Demo"
menu = "Apps"
name = "captouch demo"
category = "Demos"
[entry]
class = "App"
class = "CapTouchDemo"
[metadata]
author = "Flow3r Badge Authors"
......
from st3m import logging
from st3m.application import Application, ApplicationContext
from st3m.goose import List
from st3m.input import InputState
from ctx import Context
log = logging.Log(__name__, level=logging.INFO)
log.info("import")
import cmath
import math
import time
class Dot:
def __init__(self, size: float, imag: float, real: float) -> None:
self.size = size
self.imag = imag
self.real = real
def draw(self, i: int, ctx: Context) -> None:
imag = self.imag
real = self.real
size = self.size
col = (1.0, 0.0, 1.0)
if i % 2:
col = (0.0, 1.0, 1.0)
ctx.rgb(*col).rectangle(
-int(imag - (size / 2)), -int(real - (size / 2)), size, size
).fill()
class CapTouchDemo(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.dots: List[Dot] = []
self.last_calib = None
# self.ui_autocalib = ui.IconLabel("Autocalib done", size=30)
# self.add_event(
# event.Event(
# name="captouch_autocalib",
# action=self.do_autocalib,
# condition=lambda data: data["type"] == "button"
# and data["index"] == 0
# and data["change"]
# and data["from"] == 2,
# enabled=True,
# )
# )
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
self.dots = []
for i in range(10):
petal = ins.captouch.petals[i]
(rad, phi) = petal.position
size = 4
if petal.pressed:
size += 4
x = 70 + (rad / 1000) + 0j
x += ((-phi) / 600) * 1j
rot = cmath.exp(-2j * math.pi * i / 10)
x = x * rot
self.dots.append(Dot(size, x.imag, x.real))
def draw(self, ctx: Context) -> None:
# print(self.last_calib)
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
for i, dot in enumerate(self.dots):
dot.draw(i, ctx)
# if not self.last_calib is None and self.last_calib > 0:
# self.last_calib -= 1
# self.ui_autocalib.draw(ctx)
# def do_autocalib(self, data) -> None:
# log.info("Performing captouch autocalibration")
# captouch.calibration_request()
# self.last_calib = 50
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(CapTouchDemo(ApplicationContext()))
import captouch
import bl00mbox
blm = bl00mbox.Channel("Harmonic Demo")
import leds
import errno
from st3m.goose import List
from st3m.input import InputState
from st3m.goose import Optional
from st3m.ui.view import ViewManager
from ctx import Context
import cmath
import math
import json
import random
chords = [
[-4, 0, 3, 8, 10],
[-3, 0, 5, 7, 12],
[-1, 2, 5, 7, 11],
[0, 3, 7, 12, 14],
[3, 7, 10, 14, 15],
]
from st3m.ui import colours
tai = math.tau * 1j
from st3m.application import Application, ApplicationContext
class chord_organ_synth(bl00mbox.patches._Patch):
def __init__(self, chan):
super().__init__(chan)
self.plugins.osc = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env.signals.decay = 500
self.plugins.env.signals.release = 800
self.plugins.env.signals.attack = 100
self.plugins.env.signals.sustain = 25979
self.plugins.env.signals.input = self.plugins.osc.signals.output
self.plugins.osc_harm1 = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env_harm1 = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env_harm1.signals.decay = 500
self.plugins.env_harm1.signals.release = 1200
self.plugins.env_harm1.signals.attack = 20
self.plugins.env_harm1.signals.sustain = 25979
self.plugins.env_harm1.signals.gain.dB = -9
self.plugins.env_harm1.signals.input = self.plugins.osc_harm1.signals.output
self.plugins.osc_harm2 = self._channel.new(bl00mbox.plugins.osc)
self.plugins.env_harm2 = self._channel.new(bl00mbox.plugins.env_adsr)
self.plugins.env_harm2.signals.decay = 500
self.plugins.env_harm2.signals.release = 1400
self.plugins.env_harm2.signals.attack = 300
self.plugins.env_harm2.signals.sustain = 25979
self.plugins.env_harm2.signals.gain.dB = -18
self.plugins.env_harm2.signals.input = self.plugins.osc_harm2.signals.output
self.plugins.mixer = self._channel.new(bl00mbox.plugins.mixer, 3)
self.plugins.mixer.signals.gain.dB -= 3
# self.plugins.mixer.signals.block_dc.switch.ON = True
for i, x in enumerate(
[self.plugins.env, self.plugins.env_harm1, self.plugins.env_harm2]
):
self.plugins.mixer.signals.input[i] = x.signals.output
self.plugins.multipitch = self._channel.new(bl00mbox.plugins.multipitch, 2)
self.plugins.multipitch.signals.thru = self.plugins.osc.signals.pitch
self.plugins.multipitch.signals.output[0] = self.plugins.osc_harm1.signals.pitch
self.plugins.multipitch.signals.output[1] = self.plugins.osc_harm2.signals.pitch
self.plugins.multipitch.signals.trigger_thru = self.plugins.env.signals.trigger
self.plugins.multipitch.signals.trigger_thru = (
self.plugins.env_harm1.signals.trigger
)
self.plugins.multipitch.signals.trigger_thru = (
self.plugins.env_harm2.signals.trigger
)
self.plugins.multipitch.signals.shift[0].tone = 12 + 7
self.plugins.multipitch.signals.shift[1].tone = 24 + 4
self.signals.pitch = self.plugins.multipitch.signals.input
self.signals.output = self.plugins.mixer.signals.output
self.signals.trigger = self.plugins.multipitch.signals.trigger_in
def tone_to_note_name(tone):
# TODO: add this to radspa helpers
sct = tone * 200 + 18367
return bl00mbox.helpers.sct_to_note_name(sct)
def note_name_to_tone(note_name):
# TODO: add this to radspa helpers
sct = bl00mbox.helpers.note_name_to_sct(note_name)
return (sct - 18367) / 200
def bottom_petal_to_rgb(petal, soft=0):
petal = int(petal) % 5
ret = None
if petal == 0:
ret = (1.0, 0.5, 0)
elif petal == 1:
ret = (0, 1.0, 1.0)
elif petal == 2:
ret = (0, 0, 1.0)
elif petal == 3:
ret = (1.0, 0, 1.0)
elif petal == 4:
ret = (0, 1.0, 0)
if soft > 0 and soft < 1:
return tuple([x * (1 - soft) + soft for x in ret])
else:
return ret
def interval_to_rgb(interval):
interval = int(interval) % 12
if interval == 0: # octave: neutral, light green
return (0.5, 1, 0.5)
if interval == 1: # flat 9th: spicy, purple
return (1, 0, 1)
if interval == 2: # 9th: mellow, blue
return (0, 0, 1)
if interval == 3: # minor 3rd: gritty warm, red
return (1.0, 0, 0)
if interval == 4: # major 3rd: warm, orange
return (1.0, 0.5, 0)
if interval == 5: # 4th: natural, green
return (0, 0.9, 0.3)
if interval == 6: # tritone, neon yellow
return (0.8, 1.0, 0)
if interval == 7: # 5th: reliable, teal
return (0, 0.7, 0.9)
if interval == 8: # augmented 5th: loud, cyan
return (0, 1.0, 1.0)
if interval == 9: # 13th: pink
return (1.0, 0.6, 0.5)
if interval == 10: # 7th: generic, lime green
return (0.0, 0.9, 0.3)
if interval == 11: # major 7th: peaceful, sky blue
return (0.7, 0.7, 1.0)
class Chord:
_triads = ["diminished", "minor", "major", "augmented", "sus2", "sus4"]
def __init__(self):
self._root = 0
self.triad = "major"
self.seven = False
self.nine = False
self.j = False
self.voicing = 0
self._num_voicings = 2
self.max_slew_rate = 0
@property
def root(self):
return self._root
@root.setter
def root(self, val):
if val > 12:
return
if val < -24:
return
self._root = val
def __repr__(self):
ret = tone_to_note_name(self.root)
while ret[-1].isdigit() or ret[-1] == "-":
ret = ret[:-1]
if self.triad == "augmented":
ret += " aug. "
elif self.triad == "diminished":
ret += " dim. "
elif self.triad == "minor":
ret += "-"
elif self.triad == "sus2":
ret += "sus2 "
elif self.triad == "sus4":
ret += "sus4 "
if self.seven:
if self.j:
ret += "j7 "
else:
ret += "7 "
if self.nine:
if self.nine == "#":
ret += "#9"
elif self.nine == "b":
ret += "b9"
else:
ret += "9"
if ret[-1] == " ":
ret = ret[:-1]
return ret
def triad_incr(self):
i = Chord._triads.index(self.triad)
i = (i + 1) % len(Chord._triads)
self.triad = Chord._triads[i]
def voicing_incr(self):
self.voicing = (self.voicing + 1) % self._num_voicings
def nine_incr(self):
if self.nine == "#":
self.nine = False
elif self.nine == "b":
self.nine = True
elif self.nine:
self.nine = "#"
else:
self.nine = "b"
def seven_incr(self):
if self.seven:
if self.j:
self.seven = False
self.j = False
else:
self.seven = True
self.j = True
else:
self.seven = True
self.j = False
@property
def notes(self):
chord = [0] * 5
if self.voicing == 0:
chord[0] = self.root
# TRIADE
if self.triad == "augmented":
chord[1] = self.root + 4
chord[2] = self.root + 8
chord[4] = self.root + 16
elif self.triad == "minor":
chord[1] = self.root + 3
chord[2] = self.root + 7
chord[4] = self.root + 15
elif self.triad == "diminished":
chord[1] = self.root + 3
chord[2] = self.root + 6
chord[4] = self.root + 15
elif self.triad == "sus2":
chord[1] = self.root + 2
chord[2] = self.root + 7
chord[4] = self.root + 14
elif self.triad == "sus4":
chord[1] = self.root + 5
chord[2] = self.root + 7
chord[4] = self.root + 17
else: # self.triad == "major":
chord[1] = self.root + 4
chord[2] = self.root + 7
chord[4] = self.root + 16
# SEVENTH
if self.seven:
if self.j:
chord[3] = self.root + 11
else:
chord[3] = self.root + 10
else:
chord[3] = self.root + 12
# NINETH
if self.nine:
if self.nine == "#":
chord[4] = self.root + 15
elif self.nine == "b":
chord[4] = self.root + 13
else:
chord[4] = self.root + 14
else:
if self.seven:
chord[4] = self.root + 12
elif self.voicing == 1:
# TRIADE
if self.triad == "augmented":
chord[0] = self.root + 4 - 12
chord[1] = self.root + 8 - 12
chord[4] = self.root + 16
elif self.triad == "minor":
chord[0] = self.root + 3 - 12
chord[1] = self.root + 7 - 12
chord[4] = self.root + 15
elif self.triad == "diminished":
chord[0] = self.root + 3 - 12
chord[1] = self.root + 6 - 12
chord[4] = self.root + 15
else: # self.triad == "major":
chord[0] = self.root + 4 - 12
chord[1] = self.root + 7 - 12
chord[4] = self.root + 16
# SEVENTH
if self.seven:
if self.j:
chord[2] = self.root + 11 - 12
chord[3] = self.root
chord[4] = chord[0] + 12
else:
chord[2] = self.root + 10 - 12
chord[3] = self.root
chord[4] = chord[0] + 12
else:
chord[2] = self.root
chord[3] = chord[0] + 12
chord[4] = chord[1] + 12
# NINETH
if self.nine:
if self.nine == "#":
chord[3] = self.root + 15 - 12
elif self.nine == "b":
chord[3] = self.root + 13 - 12
else:
chord[3] = self.root + 14 - 12
return chord
class HarmonicApp(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
self.color_intensity = 0.0
self.chord_index = 0
self.chord: List[int] = []
self.synths = [blm.new(bl00mbox.patches.tinysynth) for i in range(5)]
self.chord = None
self.chords = [Chord() for x in range(5)]
self.cp_prev = captouch.read()
self.blm = None
self.prev_captouch = [0] * 10
self.fade = [0] * 5
self.mode = 0
self._set_chord(3, force_update=True)
self._num_modes = 3
for i, synth in enumerate(self.synths):
synth.signals.decay = 500
synth.signals.waveform = 0
synth.signals.attack = 50
synth.signals.volume = 0.3 * 32767
synth.signals.sustain = 0.9 * 32767
synth.signals.release = 800
synth.signals.output = blm.mixer
self._file_settings = None
self._set_chord(3)
self.prev_captouch = [0] * 10
def _try_load_settings(self, path):
try:
with open(path, "r") as f:
return json.load(f)
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _try_write_default_settings(self, path: str, default_path: str) -> None:
with open(path, "w") as outfile, open(default_path, "r") as infile:
outfile.write(infile.read())
def _try_save_settings(self, path, settings):
try:
with open(path, "w+") as f:
f.write(json.dumps(settings))
f.close()
except OSError as e:
if e.errno != errno.ENOENT:
raise # ignore file not found
def _save_settings(self):
default_path = self.app_ctx.bundle_path + "/harmonic_demo-default.json"
settings_path = "/flash/harmonic_demo.json"
settings = self._try_load_settings(default_path)
assert settings is not None, "failed to load default settings"
file_is_different = False
user_settings = self._try_load_settings(settings_path)
if user_settings is None:
file_is_different = True
for i, chord in enumerate(settings["chords"]):
chord["triad"] = self.chords[i].triad
chord["j"] = self.chords[i].j
chord["seven"] = self.chords[i].seven
chord["nine"] = self.chords[i].nine
chord["root"] = self.chords[i].root
chord["voicing"] = self.chords[i].voicing
chord["tones_readonly"] = self.chords[i].notes
if not file_is_different:
user_chord = user_settings["chords"][i]
if self._file_settings is None:
file_is_different = True
else:
file_chord = self._file_settings["chords"][i]
for i in chord:
if chord.get(i) != file_chord.get(i):
file_is_different = True
break
if file_is_different:
self._try_save_settings(settings_path, settings)
self._file_settings = settings
def _load_settings(self):
default_path = self.app_ctx.bundle_path + "/harmonic_demo-default.json"
settings_path = "/flash/harmonic_demo.json"
settings = self._try_load_settings(default_path)
assert settings is not None, "failed to load default settings"
user_settings = self._try_load_settings(settings_path)
if user_settings is None:
self._try_write_default_settings(settings_path, default_path)
else:
settings.update(user_settings)
self._file_settings = settings
def _set_chord(self, i: int) -> None:
hue = int(72 * (i + 0.5)) % 360
if i != self.chord_index:
for i, chord in enumerate(settings["chords"]):
self.chords[i].triad = chord["triad"]
self.chords[i].j = chord["j"]
self.chords[i].seven = chord["seven"]
self.chords[i].nine = chord["nine"]
self.chords[i].root = chord["root"]
self.chords[i].voicing = chord["voicing"]
def _build_synth(self):
if self.blm is not None:
return
self.blm = bl00mbox.Channel("chord organ")
self.blm.volume = 32767
self.synths = [self.blm.new(chord_organ_synth) for i in range(5)]
for synth in self.synths:
synth.signals.output = self.blm.mixer
def _set_chord(self, i, force_update=False):
if i != self.chord_index or force_update:
self.chord_index = i
leds.set_all_hsv(hue, 1, 0.2)
leds.update()
self.chord = chords[i]
self.chord = self.chords[i]
self.leds_rgb = bottom_petal_to_rgb(self.chord_index, soft=0)
self.hue_change = True
def draw(self, ctx: Context) -> None:
i = self.color_intensity
ctx.rgb(i, i, i).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.line_width = 4
ctx.get_font_name(4)
ctx.text_align = ctx.CENTER
ctx.rgb(0, 0, 0)
ctx.scope()
ctx.font_size = 20
for top_petal in range(5):
note_name = tone_to_note_name(self.chord.notes[top_petal])
interval = self.chord.notes[top_petal] - self.chord.root
color = interval_to_rgb(interval)
ctx.rgb(*color)
note_name = "".join([x for x in note_name if not x.isdigit()])
pos = 95 * cmath.exp(tai * (top_petal - 2.5) / 5)
end_pos = pos * (1 - 0.3j) * 1.27
start_pos = pos * (1 + 0.3j) * 1.27
mid_pos = (start_pos + end_pos) / 8
fade = self.fade[top_petal]
if fade > 0:
ctx.rgb(fade / 4, 0, fade / 4)
ctx.move_to(start_pos.imag, start_pos.real)
ctx.quad_to(mid_pos.imag, mid_pos.real, end_pos.imag, end_pos.real)
ctx.fill()
ctx.rgb(*color)
ctx.move_to(start_pos.imag, start_pos.real)
ctx.quad_to(mid_pos.imag, mid_pos.real, end_pos.imag, end_pos.real)
ctx.stroke()
ctx.move_to(pos.imag * 1.07, pos.real * 1.07)
ctx.text(note_name)
if self.mode == 0:
ctx.save()
ctx.font_size = 25
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
chord = self.chords[bottom_petal]
ctx.move_to(0, 0)
if bottom_petal == 3:
pos = -pos
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
if bottom_petal != self.chord_index:
ctx.rgb(*bottom_petal_to_rgb(bottom_petal, soft=0.3))
text = self.chords[bottom_petal].__repr__()
shift = 1
if len(text) > 8:
shift += 0.05 * (len(text) - 8)
if abs(pos * shift) > 120:
ctx.font_size = 20
shift = abs(120 / pos)
ctx.move_to(pos * shift, 0)
ctx.text(text)
ctx.font_size = 25
ctx.restore()
ctx.save()
ctx.font_size = 35
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -90
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
# lazy
chord = self.chords[bottom_petal]
ctx.move_to(0, 0)
if bottom_petal == 3:
pos = -pos
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
if bottom_petal == self.chord_index:
ctx.rgb(*bottom_petal_to_rgb(bottom_petal))
text = self.chords[bottom_petal].__repr__()
shift = 1
if len(text) > 8:
shift += 0.05 * (len(text) - 8)
if abs(pos * shift) > 120:
shift = abs(120 / pos)
ctx.font_size = 30
ctx.move_to(pos * shift, 0)
ctx.text(text)
ctx.font_size = 35
ctx.restore()
if self.mode == 1:
ctx.font_size = 25
ctx.rgb(*bottom_petal_to_rgb(self.chord_index, soft=0.15))
ctx.save()
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
ctx.move_to(0, 0)
if bottom_petal == 3:
ctx.text_align = ctx.RIGHT
pos = -pos
ctx.rotate(-math.tau / 2)
ctx.rotate(-math.tau / 5)
ctx.move_to(pos, 0)
if bottom_petal == 0:
ctx.text("oct+")
if bottom_petal == 4:
ctx.text("tone+")
if bottom_petal == 3:
ctx.text("tone-")
if bottom_petal == 1:
ctx.text("oct-")
ctx.restore()
ctx.move_to(0, 0)
ctx.font_size = 35
ctx.text(tone_to_note_name(self.chords[self.chord_index].root))
if self.mode == 2:
ctx.font_size = 20
ctx.rgb(*bottom_petal_to_rgb(self.chord_index, soft=0.15))
chord = self.chords[self.chord_index]
ctx.save()
ctx.rotate(math.tau * (3 / 5 - 1 / 4))
pos = -105
ctx.text_align = ctx.LEFT
for bottom_petal in range(5):
ctx.move_to(0, 0)
if bottom_petal == 3:
ctx.text_align = ctx.RIGHT
ctx.rotate(-math.tau / 2)
pos = -pos
ctx.rotate(-math.tau / 5)
ctx.move_to(pos, 0)
if bottom_petal == 4:
ctx.move_to(pos * 1.05, 0)
ctx.text("voice " + str(chord.voicing))
if bottom_petal == 0:
if chord.nine == "#":
ctx.text("#9")
elif chord.nine == "b":
ctx.text("b9")
elif chord.nine:
ctx.text("9")
else:
ctx.text("no9")
if bottom_petal == 1:
if chord.seven:
if chord.j:
ctx.text("j7")
else:
ctx.text("7")
else:
ctx.text("no7")
if bottom_petal == 3:
if chord.triad == "diminished":
ctx.text("dim.")
elif chord.triad == "augmented":
ctx.text("aug.")
else:
ctx.text(chord.triad)
ctx.restore()
ctx.move_to(0, 0)
text = self.chords[self.chord_index].__repr__()
if len(text) > 9:
ctx.font_size = 30
else:
ctx.font_size = 35
ctx.text(text)
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
if self.color_intensity > 0:
self.color_intensity -= self.color_intensity / 20
cts = captouch.read()
for i in range(10):
if cts.petals[i].pressed and (not self.cp_prev.petals[i].pressed):
if i % 2:
k = int((i - 1) / 2)
self._set_chord(k)
else:
k = int(i / 2)
self.synths[k].signals.pitch.tone = self.chord[k]
self.synths[k].signals.trigger.start()
self.color_intensity = 1.0
elif (not cts.petals[i].pressed) and self.cp_prev.petals[i].pressed:
if (1 + i) % 2:
k = int(i / 2)
self.synths[k].signals.trigger.stop()
self.cp_prev = cts
# For running with `mpremote run`:
if self.blm is None:
return
if self.input.buttons.app.left.pressed:
self.mode = (self.mode - 1) % self._num_modes
elif self.input.buttons.app.right.pressed:
self.mode = (self.mode + 1) % self._num_modes
cp = captouch.read()
modulate_leds = False
for i in range(0, 10, 2):
j = (10 - i) % 10
if cp.petals[j].pressed:
modulate_leds = True
if not self.cp_prev.petals[j].pressed:
self.synths[i // 2].signals.pitch.tone = self.chord.notes[i // 2]
self.synths[i // 2].signals.trigger.start()
self.fade[i // 2] = 1
else:
if self.fade[i // 2] > 0:
self.fade[i // 2] -= self.fade[i // 2] * float(delta_ms) / 1000
if self.fade[i // 2] < 0.05:
self.fade[i // 2] = 0
if self.cp_prev.petals[j].pressed:
self.synths[i // 2].signals.trigger.stop()
for j in range(5):
if cp.petals[1 + 2 * j].pressed:
if not self.cp_prev.petals[1 + 2 * j].pressed:
if self.mode == 0:
self._set_chord((4 - j) % 5)
elif self.mode == 1:
if j == 0:
self.chord.root += 1
elif j == 1:
self.chord.root -= 1
elif j == 3:
self.chord.root -= 12
elif j == 4:
self.chord.root += 12
elif self.mode == 2:
if j == 3:
self.chord.seven_incr()
elif j == 4:
self.chord.nine_incr()
elif j == 1:
self.chord.triad_incr()
elif j == 0:
self.chord.voicing_incr()
if self.hue_change:
leds.set_slew_rate(min(self.max_slew_rate, 200))
leds.set_all_rgb(*self.leds_rgb)
self.hue_change = False
leds.update()
elif modulate_leds:
leds.set_slew_rate(min(self.max_slew_rate, 135))
h, s, v = colours.rgb_to_hsv(*self.leds_rgb)
led_index = int(random.random() * 39)
hue = h + 2 * (random.random() - 0.5)
leds.set_rgb(led_index, *colours.hsv_to_rgb(hue, s, v))
leds.update()
else:
leds.set_slew_rate(min(self.max_slew_rate, 135))
leds.set_all_rgb(*self.leds_rgb)
leds.update()
self.cp_prev = cp
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
self.mode = 0
if self.blm is None:
self._build_synth()
self.blm.foreground = True
self._load_settings()
self.max_slew_rate = leds.get_slew_rate()
leds.set_slew_rate(min(self.max_slew_rate, 130))
self._set_chord(self.chord_index, force_update=True)
def on_exit(self):
if self.blm is not None:
self.blm.free = True
self.blm = None
self._save_settings()
def get_help(self):
if self.mode == 0:
ret = (
"Page: Chord Switcher\n"
"(cycle with app button l/r)\n\n"
'This is intended as the "main playing field:" '
"You can use the bottom petals to select a chord "
"and the top petals to play notes from that chord."
)
elif self.mode == 1:
ret = (
"Page: Root Shifter\n"
"(cycle with app button l/r)\n\n"
"You can use the bottom petals to shift the root "
"of the current chord by either semitones (petal 1+3) "
"or whole octaves (petal 7+9). The center label "
"indicates current root note and octave in scientific "
"pitch notation.\n\n"
"Tip: If you find yourself with two chord shapes that "
"you like but don't quite fit together try shifting "
"them relative to each other."
)
elif self.mode == 2:
ret = (
"Page: Chord Shaper\n"
"(cycle with app button l/r)\n\n"
"The bottom petals cycle through different abstract "
"qualities that set the shape of the current chord:\n\n"
"Petal 1: Voicing\n"
"The voicing refers to the arrangement of chord tones: "
"A chord tone may be present in any octave, or multiple "
"ones, or none at all (sometimes). Voicings can change the "
'"feel" of a chord while often retaining '
"the (dis)harmonic relationship with other chords.\n\n"
"Petal 3: Triad\n"
"Triads is probably the most well-known concept about chords: "
"Major and minor chords are triads, but there's a few more. "
"These are often used as a foundation "
'and then "spiced up" with additional "tension" notes. Petals 7+9 '
'are responsible for those; if they are set to "no7" and "no9" '
"respectively you'll get only the basic triad.\n\n"
"Petal 7: 7th\n"
"The 7th comes in 2 forms, the regular kind which goes well "
"which just about everything and the major 7th (denoted as j7) "
"may result in very bright and/or sharp shapes.\n\n"
"Petal 9: 9th\n"
"The 9th comes in 3 forms: As with the 7th the regular one is "
"fairly mellow while the b9 is notoriously dramatic in just "
"about any context. The #9 is a strange one as it is tonally "
"identical to the minor 3rd: With minor or diminished triads "
"(which have that tone in the triad) it doesn't add much, but "
'with major or augmented it can have a "double triad" effect.\n\n'
"There's a lot more tensions out there in the wild, and also "
"entirely different concepts to build harmony with, this merely "
"aims to be a compact example of chord building.\n\n"
"The notation used for chords is a bit simplified in order to "
"make it a bit more intuitive; the real world is full of special "
"names and shorthands that may make pattern recognition harder "
"so we're not using most of them here."
)
ret += (
"\n\nYour settings are saved to and loaded from "
"/flash/sys/harmonic_demo.json when exiting and entering. "
"Note for developers: Saving on flash is bad practice, do not replicate "
"(see Documentation.) This will be moved in the future."
)
return ret
if __name__ == "__main__":
import st3m.run
from st3m.run import run_app
st3m.run.run_view(HarmonicApp(ApplicationContext()))
run_app(HarmonicApp, "/flash/sys/apps/demo_harmonic")
[app]
name = "Harmonic"
menu = "Music"
name = "chord organ"
category = "Music"
[entry]
class = "HarmonicApp"
......
{
"chords": [
{ "triad": "major", "root": -4, "j": true, "voicing": 0, "nine": true, "seven": false},
{ "triad": "major", "root": 5, "j": false, "voicing": 1, "nine": true, "seven": false},
{ "triad": "major", "root": 7, "j": false, "voicing": 1, "nine": false, "seven": true},
{ "triad": "minor", "root": 0, "j": false, "voicing": 0, "nine": true, "seven": false},
{ "triad": "major", "root": 3, "j": true, "voicing": 0, "nine": false, "seven": true}
]
}
......@@ -43,4 +43,4 @@ class IMUDemo(Application):
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(IMUDemo(ApplicationContext()))
st3m.run.run_app(IMUDemo)
[app]
name = "IMU Demo"
menu = "Apps"
category = "Demos"
[entry]
class = "IMUDemo"
......
import bl00mbox
blm = bl00mbox.Channel("Melodic Demo")
import leds
from st3m.goose import List, Optional
from st3m.input import InputState, InputController
from st3m.ui.view import ViewManager
from ctx import Context
octave = 0
synths: List[bl00mbox.patches.tinysynth] = []
scale = [0, 2, 4, 5, 7, 9, 11]
def highlight_bottom_petal(num: int, r: int, g: int, b: int) -> None:
start = 4 + 8 * num
for i in range(7):
leds.set_rgb(((i + start) % 40), r, g, b)
def change_playing_field_color(r: int, g: int, b: int) -> None:
highlight_bottom_petal(0, r, g, b)
highlight_bottom_petal(1, r, g, b)
highlight_bottom_petal(3, r, g, b)
highlight_bottom_petal(4, r, g, b)
highlight_bottom_petal(2, 55, 0, 55)
leds.set_rgb(18, 55, 0, 55)
leds.set_rgb(19, 55, 0, 55)
leds.set_rgb(27, 55, 0, 55)
leds.set_rgb(28, 55, 0, 55)
leds.update()
def adjust_playing_field_to_octave() -> None:
global octave
if octave == -1:
change_playing_field_color(0, 0, 55)
elif octave == 0:
change_playing_field_color(0, 27, 27)
elif octave == 1:
change_playing_field_color(0, 55, 0)
def run(input: InputController) -> None:
global scale
global octave
global synths
any_down = False
for i in range(10):
petal = input.captouch.petals[i].whole
if petal.down:
any_down = True
if petal.pressed:
any_down = True
if i == 6:
octave = -1
adjust_playing_field_to_octave()
elif i == 5:
octave = 0
adjust_playing_field_to_octave()
elif i == 4:
octave = 1
adjust_playing_field_to_octave()
else:
k = 10 - i
if k > 3:
k -= 10
k = 3 - k
note = scale[k] + 12 * octave
synths[0].signals.pitch.tone = note
synths[0].signals.trigger.start()
if not any_down:
synths[0].signals.trigger.stop()
def init() -> None:
global synths
for i in range(1):
synth = blm.new(bl00mbox.patches.tinysynth)
synth.signals.output = blm.mixer
synths += [synth]
for synth in synths:
synth.signals.decay = 100
def foreground() -> None:
adjust_playing_field_to_octave()
from st3m.application import Application, ApplicationContext
from ctx import Context
import bl00mbox
import leds
import math
# TODO(q3k): properly port this app
class MelodicApp(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
init()
self.synths: List[bl00mbox.patches.tinysynth] = []
self.base_scale = [0, 2, 4, 5, 7, 9, 11]
self.mid_point = 7
self.mid_point_petal = 0
self.mid_point_lock = False
self.mid_point_petal_hyst = 3
self.min_note = -36
self.max_note = +30
self.at_min_note = False
self.at_max_note = False
self.auto_color = (1, 0.5, 1)
self.min_hue = 0
self.max_hue = 0
self.scale = [0] * 10
self.prev_note = None
self.legato = False
self.blm = None
def base_scale_get_val_from_mod_index(self, index):
o = index // len(self.base_scale)
i = index % len(self.base_scale)
return 12 * o + self.base_scale[i]
def base_scale_get_mod_index_from_val(self, val):
val = int(val)
index = val
while True:
try:
i = self.base_scale.index(index % 12)
break
except:
index -= 1
o = val // 12
return i + len(self.base_scale) * o
def make_scale(self):
i = self.base_scale_get_mod_index_from_val(self.mid_point)
for j in range(-5, 5):
tone = self.base_scale_get_val_from_mod_index(i + j)
self.scale[(self.mid_point_petal + j) % 10] = tone
def draw(self, ctx: Context) -> None:
ctx.rgb(1, 1, 1).rectangle(-120, -120, 240, 240).fill()
ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
ctx.text_align = ctx.CENTER
ctx.get_font_name(4)
ctx.font_size = 20
ctx.rgb(0.8, 0.8, 0.8)
ctx.move_to(0, -15)
ctx.text("note shift:")
ctx.font_size = 25
ctx.move_to(0, 15)
if self.mid_point_lock:
ctx.rgb(0, 0.5, 1)
ctx.text("manual")
else:
ctx.rgb(*self.auto_color)
ctx.text("auto")
ctx.rgb(0, 0.5, 1)
ctx.rotate(math.tau * ((self.mid_point_petal / 10) - 0.05))
tmp = ctx.line_width
ctx.line_width = 30
ctx.arc(0, 0, 105, -math.tau * 0.7, math.tau * 0.2, 0).stroke()
if not self.mid_point_lock:
ctx.rgb(*self.auto_color)
if not self.at_max_note:
ctx.arc(0, 0, 105, math.tau * 0.05, math.tau * 0.2, 0).stroke()
if not self.at_min_note:
ctx.arc(0, 0, 105, -math.tau * 0.7, -math.tau * 0.55, 0).stroke()
ctx.line_width = tmp
if self.mid_point_lock:
ctx.rgb(1, 1, 1)
else:
ctx.rgb(0, 0, 0)
ctx.scope()
ctx.fill()
ctx.rotate(-math.tau / 10)
if not self.at_max_note:
ctx.move_to(0, 110)
ctx.text("<<<")
ctx.move_to(0, 0)
if not self.at_min_note:
ctx.rotate(math.tau / 5)
ctx.move_to(0, 110)
ctx.text(">>>")
def _build_synth(self):
if self.blm is None:
self.blm = bl00mbox.Channel("melodic demo")
self.synths = []
for i in range(1):
synth = self.blm.new(bl00mbox.patches.tinysynth)
synth.signals.output = self.blm.mixer
self.synths += [synth]
for synth in self.synths:
synth.signals.decay = 100
def update_leds(self):
for i in range(40):
hue_deg = ((i * 90 / 40) + (self.mid_point * 270 / 60) + 180) % 360
if i == 0:
self.hue_min = hue_deg
if i == 39:
self.hue_max = hue_deg
index = i + (self.mid_point_petal - 5) * 4
leds.set_hsv(index % 40, hue_deg, 1, 1)
leds.update()
def on_enter(self, vm: Optional[ViewManager]) -> None:
super().on_enter(vm)
foreground()
if self.blm is None:
self._build_synth()
self.blm.foreground = True
self.make_scale()
def on_exit(self):
if self.blm is not None:
self.blm.free = True
self.blm = None
def shift_playing_field_by_num_petals(self, num):
num_positive = True
if num < 0:
num_positive = False
self.at_max_note = False
elif num > 0:
self.at_min_note = False
num = abs(num)
while num != 0:
if num > 3:
num_part = 3
num -= 3
else:
num_part = num
num = 0
if num_positive:
self.mid_point_petal += num_part
self.mid_point_petal = self.mid_point_petal % 10
else:
self.mid_point_petal -= num_part
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
# make sure things stay in bounds
while max(self.scale) > self.max_note:
self.mid_point_petal -= 1
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
self.at_max_note = True
while min(self.scale) < self.min_note:
self.mid_point_petal += 1
self.mid_point_petal = self.mid_point_petal % 10
self.mid_point = self.scale[self.mid_point_petal]
self.make_scale()
self.at_min_note = True
self.make_scale()
if max(self.scale) == self.max_note:
self.at_max_note = True
if min(self.scale) == self.min_note:
self.at_min_note = True
def think(self, ins: InputState, delta_ms: int) -> None:
if self.blm is None:
return
super().think(ins, delta_ms)
run(self.input)
petals = []
if self.input.buttons.app.middle.pressed:
self.mid_point_lock = not self.mid_point_lock
if self.input.buttons.app.right.pressed:
self.shift_playing_field_by_num_petals(4)
if self.input.buttons.app.left.pressed:
self.shift_playing_field_by_num_petals(-4)
for i in range(10):
if ins.captouch.petals[i].pressed:
petals += [i]
if len(petals) == 0:
self.synths[0].signals.trigger.stop()
self.prev_note = None
else:
if (len(petals) == 1) and (not self.mid_point_lock):
delta = petals[0] - self.mid_point_petal
if delta > 4:
delta -= 10
if delta < -5:
delta += 10
if delta > 2:
self.shift_playing_field_by_num_petals(delta - 2)
if delta < -3:
self.shift_playing_field_by_num_petals(delta + 3)
avg = 0
for petal in petals:
avg += self.scale[petal]
avg /= len(petals)
self.synths[0].signals.pitch.tone = avg
if (self.legato and self.prev_note is None) or (
(not self.legato) and self.prev_note != avg
):
self.synths[0].signals.trigger.start()
self.prev_note = avg
self.update_leds()
# For running with `mpremote run`:
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(MelodicApp(ApplicationContext()))
st3m.run.run_app(MelodicApp)
[app]
name = "Melodic"
menu = "Music"
name = "melodic demo"
category = "Hidden"
[entry]
class = "MelodicApp"
......
......@@ -18,6 +18,8 @@ class ScrollDemo(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
# this class is deprecated, please don't use it for new apps
# check out st3m.ui.widgets.Scroller instead!
self.scroll = CapScrollController()
def draw(self, ctx: Context) -> None:
......@@ -76,4 +78,4 @@ class ScrollDemo(Application):
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(ScrollDemo(ApplicationContext()))
st3m.run.run_app(ScrollDemo)
[app]
name = "Scroll Demo"
menu = "Apps"
category = "Demos"
[entry]
class = "ScrollDemo"
......
......@@ -21,55 +21,40 @@ class AppWorms(Application):
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx)
# HACK: we work against double buffering by keeping note of how many
# times on_draw got called.
#
# We know there's two buffers, so if we render the same state twice in a
# row we'll be effectively able to keep a persistent framebuffer, like
# with the old API.
#
# When bufn is in [0, 1] we render the background image.
# When bufn is in [2, ...) we render the worms.
# When bufn is > 3, we enable updating the worm state.
#
# TODO(q3k): allow apps to request single-buffered graphics for
# persistent framebuffers.
self.bufn = 0
self.worms = []
for i in range(0):
self.worms.append(Worm())
self.just_shown = True
self.just_shown = False
def on_enter(self, vm: Optional[ViewManager]) -> None:
# print("on foreground")
super().on_enter(vm)
self.just_shown = True
self.just_shown = False
self.worms = [] # reset worms
def draw(self, ctx: Context) -> None:
if self.bufn <= 5:
def draw_background(self, ctx):
ctx.rgb(*BLUE).rectangle(-120, -120, 240, 240).fill()
ctx.text_align = ctx.CENTER
ctx.text_baseline = ctx.MIDDLE
ctx.move_to(0, 0).rgb(*WHITE).text("touch me :)")
self.bufn += 1
return
def draw(self, ctx: Context) -> None:
if self.vm.transitioning and self.is_active():
self.draw_background(ctx)
else:
for w in self.worms:
w.draw(ctx)
self.bufn += 1
self.just_shown = True
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
# Simulation is currently locked to FPS.
if self.bufn > 7:
if self.just_shown:
for w in self.worms:
w.move()
self.bufn = 6
self.just_shown = False
for index, petal in enumerate(self.input.captouch.petals):
if petal.whole.pressed or petal.whole.repeated:
self.worms.append(Worm(-tau * index / 10 + math.pi))
......@@ -126,6 +111,7 @@ class Worm:
self.size += 1
self.speed = self.size / 5
self.speed /= 2 # temporary hack bc framerate doubling
self.direction += (random.random() - 0.5) * math.pi / 4
......@@ -146,4 +132,4 @@ class Worm:
if __name__ == "__main__":
import st3m.run
st3m.run.run_view(AppWorms(ApplicationContext()))
st3m.run.run_app(AppWorms)
[app]
name = "Worms"
menu = "Apps"
category = "Games"
[entry]
class = "AppWorms"
......
from st3m import logging
from st3m.application import Application, ApplicationContext
from st3m.goose import Optional
from st3m.input import InputState
from st3m.ui.view import View, ViewManager
from ctx import Context
......@@ -12,9 +13,11 @@ class Fil3sApp(Application):
log = logging.Log(__name__, level=logging.INFO)
path: str = "/"
selected: Optional[str] = None
def __init__(self, app_ctx: ApplicationContext) -> None:
super().__init__(app_ctx=app_ctx)
self.view = "browser"
def on_enter(self, vm: ViewManager | None) -> None:
super().on_enter(vm)
......@@ -22,19 +25,29 @@ class Fil3sApp(Application):
if self.vm is None:
raise RuntimeError("vm is None")
self.vm.replace(Browser(self.path, self.on_navigate, self.on_update_path))
if self.view == "browser":
self.vm.replace(
Browser(self.path, self.on_navigate, self.on_update_path, self.selected)
)
elif self.view == "reader":
self.vm.replace(Reader(self.path, self.on_navigate, self.on_update_path))
def on_navigate(self, view: str) -> None:
if self.vm is None:
raise RuntimeError("vm is None")
self.view = view
if view == "browser":
self.vm.replace(Browser(self.path, self.on_navigate, self.on_update_path))
self.vm.replace(
Browser(self.path, self.on_navigate, self.on_update_path, self.selected)
)
elif view == "reader":
self.vm.replace(Reader(self.path, self.on_navigate, self.on_update_path))
def on_update_path(self, path: str) -> None:
def on_update_path(self, path: str, selected: str = None) -> None:
self.path = path
self.selected = selected
def draw(self, ctx: Context) -> None:
pass
......
import os
import uos
import stat
from st3m.goose import Callable, Generator
import math
from st3m.goose import Callable, Generator, Optional
from st3m.input import InputState
from ctx import Context
......@@ -20,6 +21,8 @@ class Browser(ActionView):
up_enabled: bool = False
prev_enabled: bool = False
next_enabled: bool = False
delete_enabled: bool = True
select_enabled: bool = True
current_pos = 0
current_entry: tuple[str, str]
......@@ -28,10 +31,18 @@ class Browser(ActionView):
path: str,
navigate: Callable[[str], None],
update_path: Callable[[str], None],
selected: Optional[str] = None,
) -> None:
super().__init__()
self._delete_held_for = 0.0
self._delete_hold_time = 1.5
self._delete_require_release = False
self._scroll_pos = 0.0
self.path = path
self.selected = selected
self.navigate = navigate
self.update_path = update_path
......@@ -43,14 +54,16 @@ class Browser(ActionView):
if self.current_pos > 0:
self.current_pos -= 1
self._update_position()
if index == 3:
elif index == 3:
self._up()
if index == 2:
elif index == 2:
self._select()
elif index == 1:
if self.current_pos < len(self.dir_entries) - 1:
self.current_pos += 1
self._update_position()
elif index == 0:
self._delete()
def draw(self, ctx: Context) -> None:
utils.fill_screen(ctx, theme.BACKGROUND)
......@@ -65,22 +78,36 @@ class Browser(ActionView):
ctx.text(self.current_entry[1])
ctx.font_size = 24
ctx.move_to(0, 20)
ctx.font = "Camp Font 3"
xpos = 0.0
if (width := ctx.text_width(self.current_entry[0])) > 220:
xpos = math.sin(self._scroll_pos) * (width - 220) / 2
ctx.move_to(xpos, 20)
ctx.text(self.current_entry[0])
def think(self, ins: InputState, delta_ms: int) -> None:
super().think(ins, delta_ms)
for i in range(0, 5):
self._scroll_pos += delta_ms / 1000
# Handle delete petal being held down
if ins.captouch.petals[0].pressed:
if not self._delete_require_release:
self._delete_held_for += delta_ms / 1000
if self._delete_held_for > self._delete_hold_time:
self._delete_held_for = self._delete_hold_time
self._delete_require_release = True
self._on_action(0)
else:
self._delete_held_for = 0.0
self._delete_require_release = False
self.actions[0].progress = self._delete_held_for / self._delete_hold_time
for i in range(1, 5):
if self.input.captouch.petals[i * 2].whole.pressed:
self._on_action(i)
return
def _is_dir(self, path: str) -> bool:
st_mode = uos.stat(path)[0] # Can fail with OSError
return stat.S_ISDIR(st_mode)
def _get_dir_entry(
self, names: list[str]
) -> Generator[tuple[str, str], None, None]:
......@@ -88,7 +115,7 @@ class Browser(ActionView):
try:
if self.path + name == "/flash/sys/st3m":
yield (name, "\ue545")
elif self._is_dir(self.path + name):
elif utils.is_dir(self.path + name):
yield (name, "\ue2c7")
else:
yield (name, "\ue873")
......@@ -97,14 +124,19 @@ class Browser(ActionView):
print(f"Failed to create entry for {name}: {e}")
def _scan_path(self) -> None:
dir = os.listdir(self.path)
self.current_pos = 0
if self.selected and self.selected in dir:
self.current_pos = dir.index(self.selected)
self.dir_entries = list(self._get_dir_entry(os.listdir(self.path)))
self.dir_entries = list(self._get_dir_entry(dir))
self._update_position()
def _change_path(self, path: str) -> None:
def _change_path(self, path: str, selected: Optional[str] = None) -> None:
self.path = path
self.selected = selected
self._scan_path()
self.up_enabled = self.path != "/"
......@@ -112,15 +144,19 @@ class Browser(ActionView):
if up_action is not None:
up_action.enabled = self.up_enabled
self.update_path(self.path)
self.update_path(self.path, selected)
self._update_actions()
def _select(self) -> None:
if not self.select_enabled:
return
name = self.dir_entries[self.current_pos][0]
old_path = self.path
new_path = self.path + name
try:
if self._is_dir(new_path):
if utils.is_dir(new_path):
self._change_path(new_path + "/")
else:
self.update_path(self.path + name)
......@@ -130,22 +166,43 @@ class Browser(ActionView):
print(f"Failed to open {new_path}: {e}")
self._change_path(old_path)
def _delete(self) -> None:
if not self.delete_enabled:
return
name = self.dir_entries[self.current_pos][0]
path = self.path + name
try:
if utils.is_dir(path):
utils.rmdirs(path)
else:
os.remove(path)
print(f"deleted file: {path}")
# refresh dir listing
self.current_pos = max(self.current_pos - 1, 0)
self._scan_path()
except Exception as e:
# TODO: Create error view
print(f"Failed to delete {path}: {e}")
def _up(self) -> None:
if not self.up_enabled or len(self.path) <= 1:
return
segments = self.path[1:-1].split("/")
if len(segments) == 1:
self._change_path("/")
self._change_path("/", segments[-1])
else:
segments.pop()
self._change_path("/" + "/".join(segments) + "/")
selected = segments.pop()
self._change_path("/" + "/".join(segments) + "/", selected)
def _update_actions(self) -> None:
self.actions = [
Action(icon="\ue3e3", label="Menu", enabled=False),
Action(icon="\ue92b", label="Delete", enabled=self.delete_enabled),
Action(icon="\ue409", label="Next", enabled=self.next_enabled),
Action(icon="\ue876", label="Select"),
Action(icon="\ue876", label="Select", enabled=self.select_enabled),
Action(icon="\ue5c4", label="Back", enabled=self.up_enabled),
Action(icon="\ue5cb", label="Prev", enabled=self.prev_enabled),
]
......@@ -153,9 +210,26 @@ class Browser(ActionView):
def _update_position(self) -> None:
try:
self.current_entry = self.dir_entries[self.current_pos]
except:
self.select_enabled = True
except Exception:
self.current_entry = ("\ue002", "No files")
self.select_enabled = False
self.up_enabled = self.path != "/"
self.prev_enabled = self.current_pos > 0
self.next_enabled = self.current_pos < len(self.dir_entries) - 1
# disallow deleting st3m folder
name = self.current_entry[0]
self.delete_enabled = (
self.path + name
not in [
"/flash",
"/flash/sys",
"/flash/sys/st3m",
"/sd",
]
and self.select_enabled
)
self._scroll_pos = math.pi / 2
self._update_actions()
......@@ -13,11 +13,15 @@ class Action:
icon: str
label: str
enabled: bool
progress: float
def __init__(self, icon: str, label: str, enabled: bool = True) -> None:
def __init__(
self, icon: str, label: str, enabled: bool = True, progress: float = 0.0
) -> None:
self.icon = icon
self.label = label
self.enabled = enabled
self.progress = progress
class ActionView(BaseView):
......@@ -66,7 +70,12 @@ class ActionView(BaseView):
if action.enabled:
utils.draw_circle(
ctx, theme.PRIMARY, self.action_x[i], self.action_y[i], 18
ctx,
theme.PRIMARY,
self.action_x[i],
self.action_y[i],
18,
action.progress,
)
else:
utils.draw_circle(
......
from math import pi
from math import tau
from ctx import Context
import uos
import stat
def fill_screen(ctx: Context, color: tuple[float, float, float]) -> None:
......@@ -13,9 +15,31 @@ def fill_screen(ctx: Context, color: tuple[float, float, float]) -> None:
def draw_circle(
ctx: Context, color: tuple[float, float, float], x: int, y: int, radius: int
ctx: Context,
color: tuple[float, float, float],
x: int,
y: int,
radius: int,
progress: float = 0.0,
) -> None:
ctx.move_to(x, y)
ctx.rgb(*color)
ctx.arc(x, y, radius, -pi, pi, True)
ctx.arc(x, y, radius, tau * progress, tau, 0)
ctx.fill()
def is_dir(path: str) -> bool:
st_mode = uos.stat(path)[0] # Can fail with OSError
return stat.S_ISDIR(st_mode)
def rmdirs(base_path):
for entry in uos.listdir(base_path):
path = f"{base_path}/{entry}"
if is_dir(path):
rmdirs(path)
else:
uos.remove(path)
print(f"deleted file: {path}")
uos.rmdir(base_path)
print(f"deleted folder: {base_path}")
[app]
name = "Files"
menu = "Apps"
category = "Apps"
[entry]
class = "Fil3sApp"
......