Galactic Unicorn: Paint app.

feature/galactic_unicorn
Phil Howard 2022-10-17 16:06:28 +01:00
rodzic 70b7d3065d
commit d736342578
6 zmienionych plików z 1716 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,11 @@
# Galactic Paint
Galactic Paint lets you paint pixels onto your Galatic Unicorn over WiFi, in realtime!
## Setting Up
You'll need `WIFI_CONFIG.py` from the `common` directory to be saved to your Pico W. Open up `WIFI_CONFIG.py` in Thonny to add your wifi details (and save it when you're done).
You will also have to install `micropython-phew` and `microdot` through Thonny's Tools -> Manage Packages.
Run the example through Thonny and it should get connected and give you a URL to visit. Open that URL in your browser and start painting!

Wyświetl plik

@ -0,0 +1,113 @@
import os
from microdot_asyncio import Microdot, send_file
from microdot_asyncio_websocket import with_websocket
from phew import connect_to_wifi
from galactic import GalacticUnicorn
from picographics import PicoGraphics, DISPLAY_GALACTIC_UNICORN as DISPLAY
from WIFI_CONFIG import SSID, PSK
gu = GalacticUnicorn()
graphics = PicoGraphics(DISPLAY)
mv_graphics = memoryview(graphics)
gu.set_brightness(0.5)
WIDTH, HEIGHT = graphics.get_bounds()
ip = connect_to_wifi(SSID, PSK)
print(f"Start painting at: http://{ip}")
server = Microdot()
@server.route("/", methods=["GET"])
def route_index(request):
return send_file("galactic_paint/index.html")
@server.route("/static/<path:path>", methods=["GET"])
def route_static(request, path):
return send_file(f"galactic_paint/static/{path}")
def get_pixel(x, y):
if x < WIDTH and y < HEIGHT and x >= 0 and y >= 0:
o = (y * WIDTH + x) * 4
return tuple(mv_graphics[o:o + 3])
return None
def flood_fill(x, y, r, g, b):
todo = []
def fill(x, y, c):
if get_pixel(x, y) != c:
return
graphics.pixel(x, y)
up = get_pixel(x, y - 1)
dn = get_pixel(x, y + 1)
lf = get_pixel(x - 1, y)
ri = get_pixel(x + 1, y)
if up == c:
todo.append((x, y - 1))
if dn == c:
todo.append((x, y + 1))
if lf == c:
todo.append((x - 1, y))
if ri == c:
todo.append((x + 1, y))
c = get_pixel(x, y)
if c is None:
return
fill(x, y, c)
while len(todo):
x, y = todo.pop(0)
fill(x, y, c)
@server.route('/paint')
@with_websocket
async def echo(request, ws):
while True:
data = await ws.receive()
try:
x, y, r, g, b = [int(n) for n in data[0:5]]
graphics.set_pen(graphics.create_pen(r, g, b))
graphics.pixel(x, y)
except ValueError:
if data == "show":
gu.update(graphics)
if data == "fill":
data = await ws.receive()
x, y, r, g, b = [int(n) for n in data[0:5]]
graphics.set_pen(graphics.create_pen(r, g, b))
flood_fill(x, y, r, g, b)
if data == "clear":
graphics.set_pen(graphics.create_pen(0, 0, 0))
graphics.clear()
if data == "save":
filename = await ws.receive()
print(f"Saving to {filename}.bin")
try:
os.mkdir("saves")
except OSError:
pass
with open(f"saves/{filename}.bin", "wb") as f:
f.write(graphics)
await ws.send(f"alert: Saved to saves/{filename}.bin")
server.run(host="0.0.0.0", port=80)

Wyświetl plik

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Galactic Paint</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="/static/paint.css">
</head>
<body>
<div class="window">
<h1>Galactic Paint</h1>
<table cellspacing="0" cellpadding="0" border-collapse="collapse">
<tbody></tbody>
</table>
<div id="palette">
<ul>
<li class="selected" style="background:rgb(0,0,0);"></li>
<li style="background:rgb(132,0,0);"></li>
<li style="background:rgb(0,132,0);"></li>
<li style="background:rgb(132,132,0);"></li>
<li style="background:rgb(0,0,132);"></li>
<li style="background:rgb(132,0,132);"></li>
<li style="background:rgb(0,132,132);"></li>
<li style="background:rgb(132,132,132);"></li>
<li style="background:rgb(198,198,198);"></li>
<li style="background:rgb(255,0,0);"></li>
<li style="background:rgb(0,255,0);"></li>
<li style="background:rgb(255,255,0);"></li>
<li style="background:rgb(0,0,255);"></li>
<li style="background:rgb(255,0,255);"></li>
<li style="background:rgb(0,255,255);"></li>
<li style="background:rgb(255,255,255);"></li>
</ul>
<input type="color" id="custom" name="custom" value="#ff0000">
</div>
<ul class="tools">
<li data-tool="paint" class="paint selected"><span class="fa fa-pencil"></span></li>
<li data-tool="fill" class="fill"><span class="fa fa-bitbucket"></span></li>
<li data-tool="erase" class="erase"><span class="fa fa-eraser"></span></li>
<li data-tool="pick" class="pick"><span class="fa fa-eyedropper"></span></li>
<li data-tool="lighten" class="lighten"><span class="fa fa-sun-o"></span></li>
<li data-tool="darken" class="darken"><span class="fa fa-adjust"></span></li>
<li data-tool="trash" class="trash"><span class="fa fa-trash"></span></li>
<li data-tool="save" class="save"><span class="fa fa-save"></span></li>
</ul>
</div>
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>
<script type="text/javascript" src="/static/tinycolor.js"></script>
<script type="text/javascript" src="/static/paint.js"></script>
</body>
</html>

Wyświetl plik

@ -0,0 +1,131 @@
body {
background:#333;
padding:20px;
font-family:Arial, Verdana, Sans-Serif;
background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAaUlEQVQYV33Q0Q3AIAgEUBjBFVyBFRzbWVjBEajXBIOVypcJj1NhETG61BiDVJX4Bh211v5hRDiniV+Elx0wQwd0hEatlUop65srMSah23vf8Auz65AWMc8rDHvCCjAQK2KeDcuQDzh+AHEJX8mbbU1BAAAAAElFTkSuQmCC) repeat;
}
.icons {
position:absolute;
margin:0;
padding:20px;
list-style:none;
}
.icons li {
margin:20px;
padding:0;
list-style:none;
padding-top:80px;
width:100px;
}
.icons li span {
background:#FFF;
color:#000;
border:1px solid #000;
line-height:20px;
padding:5px 10px;
text-align:center;
font-size:10px;
line-height:10px;
display:inline-block;
}
#palette ul, #palette li {
margin:0;padding:0;list-style:none;
}
#palette {
list-style:none;
position:relative;
height: 122px;
padding:0 8px;
}
#palette ul {
display:block;
width:456px;
float: left;
}
#palette li, #palette input {
border: 2px outset;
width:49px;
height:49px;
float:left;
display:block;
margin:2px;
}
#palette input {
width:110px;
height:110px;
}
.window {
width: 976px;
position: relative;
background: #0E071A;
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5);
}
.tools {
margin:0;padding:0;list-style:none;
clear:both;
display:block;
position:absolute;
top: 50px;
right: 8px;
width: 98px;
background:#999999;
font-size:0;
}
.tools span {
line-height:30px;
}
.tools li {
font-size:16px;
width: 45px;
height: 40px;
text-align:center;
margin:0;
padding:0;
display:inline-block;
line-height:40px;
border:2px outset #EEEEEE;
background:#F5F5F5;
cursor:pointer;
color:#000;
}
.tools li.selected {
background:#000;
color:#FFF;
}
h1 {
color: #FFF;
background: #6D38BB;
height:40px;
margin:0;
padding:0 8px;
line-height:40px;
font-weight:normal;
font-size:24px;
}
table {
clear:both;
cursor:pointer;
margin:10px;
border:1px solid #333;
background: #000000;
}
table td {
width:14px;
height:14px;
border:1px solid #333;
}

Wyświetl plik

@ -0,0 +1,214 @@
'use strict';
var md = false;
var color = tinycolor('#840000');
var update;
$(document).ready(function(){
var picker = $('#custom');
var palette = $('#palette');
picker.val(color.toHexString());
$(document)
.on('mousedown',function(e){md=true;})
.on('mouseup',function(e){md=false;});
$('table').on('dragstart', function(e){
e.preventDefault();
return false;
});
for (var y = 0; y < 11; y++) {
var row = $('<tr></tr>');
for (var x = 0; x < 53; x++) {
row.append('<td></td>');
}
$('tbody').append(row);
}
$('.tools li').on('click', function(){
switch($(this).index()){
case 6:
clear();
break;
case 7:
save();
break;
default:
$('.tools li').removeClass('selected');
$(this).addClass('selected');
break;
}
});
picker.on('change', function(){
color = tinycolor($(this).val());
})
palette.find('li').on('click', function(){
pick(this);
});
function handle_tool(obj, is_click){
switch($('.tools li.selected').index()){
case 0: //'paint':
paint(obj);
break;
case 1: // Fill
if( is_click ) fill(obj);
break;
case 2: // Erase
update_pixel(obj, tinycolor('#000000'));
break;
case 3: //'pick':
pick(obj);
break;
case 4: //'lighten':
lighten(obj);
break;
case 5: //'darken':
darken(obj);
break;
}
}
var fill_target = null;
var fill_stack = [];
function fill(obj){
fill_target = tinycolor($(obj).css('background-color')).toRgbString();
if( fill_target == color.toRgbString() ){
return false;
}
var x = $(obj).index();
var y = $(obj).parent().index();
socket.send("fill");
socket.send(new Uint8Array([x, y, color.toRgb().r, color.toRgb().g, color.toRgb().b]));
socket.send('show');
do_fill(obj);
while(fill_stack.length > 0){
var pixel = fill_stack.pop();
do_fill(pixel);
}
}
function is_target_color(obj){
return ( tinycolor($(obj).css('background-color')).toRgbString() == fill_target);
}
function do_fill(obj){
var obj = $(obj);
if( is_target_color(obj) ){
$(obj).css('background-color', color.toRgbString());
var r = obj.next('td'); // Right
var l = obj.prev('td'); // Left
var u = obj.parent().prev('tr').find('td:eq(' + obj.index() + ')'); // Above
var d = obj.parent().next('tr').find('td:eq(' + obj.index() + ')'); // Below
if( r.length && is_target_color(r[0]) ) fill_stack.push(r[0]);
if( l.length && is_target_color(l[0]) ) fill_stack.push(l[0]);
if( u.length && is_target_color(u[0]) ) fill_stack.push(u[0]);
if( d.length && is_target_color(d[0]) ) fill_stack.push(d[0]);
}
}
function save(){
var filename = prompt('Please enter a filename', 'mypaint');
filename = filename.replace(/[^a-z0-9]/gi, '_').toLowerCase();
socket.send('save');
socket.send(filename);
}
function clear(){
$('td').css('background-color','rgb(0,0,0)').data('changed',false);
socket.send('clear');
socket.send('show');
}
function lighten(obj){
var c = tinycolor($(obj).css('background-color'));
c.lighten(5);
update_pixel(obj, c);
}
function darken(obj){
var c = tinycolor($(obj).css('background-color'));
c.darken(5);
update_pixel(obj, c);
}
function pick(obj){
color = tinycolor($(obj).css('background-color'));
picker.val(color.toHexString());
}
function update_pixel(obj, col){
var bgcol = tinycolor($(obj).css('background-color'));
if(col != bgcol){
$(obj)
.data('changed', true)
.css('background-color', col.toRgbString());
}
}
function update_pixels(){
var changed = false;
$('td').each(function( index, obj ){
if($(obj).data('changed')){
$(obj).data('changed',false);
changed = true;
var x = $(this).index();
var y = $(this).parent().index();
var col = tinycolor($(obj).css('background-color')).toRgb();
if(socket) {
socket.send(new Uint8Array([x, y, col.r, col.g, col.b]));
}
}
});
if(changed){
socket.send('show');
}
}
function paint(obj){
update_pixel(obj, color);
}
$('table td').on('click', function(){
handle_tool(this, true);
});
$('table td').on('mousemove', function(){
if(!md) return false;
handle_tool(this, false);
})
const socket = new WebSocket('ws://' + window.location.host + '/paint');
socket.addEventListener('message', ev => {
console.log('<<< ' + ev.data);
if(ev.data.substring(0, 6) == "alert:") {
alert(ev.data.substring(6));
}
});
socket.addEventListener('close', ev => {
console.log('<<< closed');
});
socket.addEventListener('open', ev => {
clear();
update = setInterval(update_pixels, 50);
});
});