Py.Cafe

maartenbreddels/

solara-pygame-starfield

Interactive Starfield Demo using Pygame

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# on crash, click Settings->Restart system (due to an issue with pygame-ce + pycafe)
# from https://github.com/tank-king/BreakPong
import solara
import numpy as np
import pygame
import asyncio
import time

data = solara.reactive(None)
import config
width, height = 640, 480
# width = config.screen_width
# height = config.screen_height

player_pos = pygame.Vector2(width / 4, height / 2)

screen = None
canvas = None


if not hasattr(pygame, "events"):
    pygame.events = []
def addEventListener(name, callback, flags):
    pygame.events.append((name, callback))
    print(name)


### BOILERPLATE START ###
def setup():
    global screen, canvas
    import js

    canvas = js.OffscreenCanvas.new(width, height)
    canvas.id = "canvas"  # emscripten wants this?
    canvas.style = js.Object.new()  # otherwise we'll get a style.cursor error
    
    js.pyodide.canvas.setCanvas2D(canvas)

    # emscripten assumes globalThis.screen to exist
    js.screen = {
        "width": width,
        "height": height,
    }
    pygame.display.init()
    js.navigator.userActivation = js.Object.new()
    js.navigator.userActivation.isActive = True
    
    # we define a mock globalThis.window, and to
    # avoid 'eventHandler.target.addEventListener is not a function'
    # we put in this mock addEventListener
    # print("adding event hander")
    # why is this still needed?
    js.window.addEventListener = addEventListener#js.Function.new()

    js.fakeCanvas = js.Object.new()
    # used for mouse events
    # note that left and top should be the real coordinates of the canvas
    # for screenX/Y, but we override those with offsetX/Y
    js.fakeCanvas.getBoundingClientRect = js.Function.new(
        "return {left: 0, top: 0, width: %i, height: %i}" % (width, height)
    )
    js.fakeCanvas.addEventListener = addEventListener

    def querySelector(query):
        print("query", query)
        return js.fakeCanvas

    # monkey patch document
    js.document = js.Object.new()
    js.document.querySelector = querySelector
    js.document.fullscreenEnabled = False
    js.document.addEventListener = js.Function.new()

    screen = pygame.display.set_mode((width, height))

def transfer_image():
    # read off a pixel from the canvas
    context = canvas.getContext("2d")
    # can possibly be faster?
    mem_obj = context.getImageData(0, 0, width, height).data.to_py()
    np_array = np.frombuffer(mem_obj, dtype=np.uint8)
    np_array.shape = (height, width, 4)
    data.value = np_array

# ideally, all the above boilerplate goes away
### BOILERPLATE END ###

print("setup")
setup()
#!/usr/bin/env python
""" pg.examples.stars

    We are all in the gutter,
    but some of us are looking at the stars.
                                            -- Oscar Wilde

A simple starfield example. Note you can move the 'center' of
the starfield by leftclicking in the window. This example show
the basics of creating a window, simple pixel plotting, and input
event management.
"""
import random
import math
import pygame as pg

# constants
WINSIZE = [640, 480]
WINCENTER = [320, 240]
NUMSTARS = 150


def init_star(steps=-1):
    "creates new star values"
    dir = random.randrange(100000)
    steps_velocity = 1 if steps == -1 else steps * 0.09
    velmult = steps_velocity * (random.random() * 0.6 + 0.4)
    vel = [math.sin(dir) * velmult, math.cos(dir) * velmult]

    if steps is None:
        return [vel, [WINCENTER[0] + (vel[0] * steps), WINCENTER[1] + (vel[1] * steps)]]
    return [vel, WINCENTER[:]]


def initialize_stars():
    "creates a new starfield"
    random.seed()
    stars = [init_star(steps=random.randint(0, WINCENTER[0])) for _ in range(NUMSTARS)]
    move_stars(stars)
    return stars


def draw_stars(surface, stars, color):
    "used to draw (and clear) the stars"
    for _, pos in stars:
        pos = (int(pos[0]), int(pos[1]))
        surface.set_at(pos, color)


def move_stars(stars):
    "animate the star values"
    for vel, pos in stars:
        pos[0] = pos[0] + vel[0]
        pos[1] = pos[1] + vel[1]
        if not 0 <= pos[0] <= WINSIZE[0] or not 0 <= pos[1] <= WINSIZE[1]:
            vel[:], pos[:] = init_star()
        else:
            vel[0] = vel[0] * 1.05
            vel[1] = vel[1] * 1.05


async def main():
    "This is the starfield code"
    # create our starfield
    stars = initialize_stars()

    # initialize and prepare screen
    pg.init()
    screen = pg.display.set_mode(WINSIZE)
    pg.display.set_caption("pygame Stars Example")
    white = 255, 240, 200
    black = 20, 20, 40
    screen.fill(black)

    clock = pg.time.Clock()

    # main game loop
    done = 0
    while not done:
        draw_stars(screen, stars, black)
        move_stars(stars)
        draw_stars(screen, stars, white)
        pg.display.update()
        for e in pg.event.get():
            if e.type == pg.QUIT or (e.type == pg.KEYUP and e.key == pg.K_ESCAPE):
                done = 1
                break
            if e.type == pg.MOUSEBUTTONDOWN and e.button == 1:
                print(e, e.pos, pygame.mouse.get_pos(), pygame.mouse.get_rel())
                WINCENTER[:] = list(e.pos)
        clock.tick(50)
        await asyncio.sleep(0.01)
        transfer_image()
    pg.quit()


reset_counter = solara.reactive(0)
def reset():
    reset_counter.value += 1

import reacton
import ipyvue
from typing import cast
from reacton.core import get_render_context
import js

def use_events(
    el, events
):
    # to avoid add_event_handler having a stale reference to callback
    events_ref = reacton.use_ref(events)
    events_ref.current = events

    def add_event_handler():
        vue_widget = cast(ipyvue.VueWidget, reacton.core.get_widget(el))
        # we are basically copying the logic from
        # reacton.core._event_handler_exception_wrapper
        rc = get_render_context()
        context = rc.context
        assert context is not None

        def handler(widget, event_name, data):
            try:
                # avoid relying of a true bounding rect
                if "offsetX" in data:
                    data["clientX"] = data["offsetX"]
                    data["clientY"] = data["offsetY"]
                # print(event_name, data)
                for name, callback in events_ref.current:
                    if(event_name == "mousedown"):
                        print(name, data)
                    # create a fake event object
                    data_js = js.Object.new()
                    for key, value in data.items():
                        setattr(data_js, key, value)
                    data_js.preventDefault = js.Function.new()
                    if name == event_name:
                        callback(data_js)
            except Exception as e:
                assert context is not None
                # because widgets don't have a context, but are a child of a component
                # we add it to exceptions_children, not exception_self
                # this allows a component to catch the exception of a direct child
                context.exceptions_children.append(e)
                rc.force_update()

        eventnames = set([name for name, callback in events])
        print(eventnames)
        for name in eventnames:
            print("add", name)
            vue_widget.on_event(name, handler)

        def cleanup():
            for name in eventnames:
                vue_widget.on_event(name, handler, remove=True)

        return cleanup

    reacton.use_effect(add_event_handler, [events])

@solara.component
def Page():
    solara.Button(label=f"Restart", on_click=reset, outlined=True, color="primary")
    with solara.Div(attributes={'tabIndex': 0}) as div:
        solara.lab.use_task(main, dependencies=[reset_counter.value])
        if data.value is not None:
            solara.Image(data.value)
    # print(pygame.events)
    use_events(div, pygame.events)