【得捷电子Follow me第2期】任务5:通过网络控制WS2812B
[复制链接]
【得捷电子Follow me第2期】任务5:通过网络控制WS2812B
在手机上通过网络控制板载Neopixel LED的显示和颜色切换,屏幕同步显示状态。
接线:
无
编程语言和环境用CircuitPython:
开发板:
Adafruit Feather ESP32-S3 TFT
运行环境:
Adafruit CircuitPython 8.2.3 on 2023-08-11
编辑器:
mu-editor
用到的额外模块:
adafruit_httpserver
adafruit_display_text
neopixel.mpy
还需要在code.py同目录下创建一个secrets.py,存放要连接的wifi名称跟密码。
还需要在code.py同目录下创建一个mywebsite文件夹,存放index.html网页。
代码解析:
以下是secrets.py文件完整代码:(相应位置要改成自己的wifi名称跟密码,其他几个暂时没用上)
secrets={
"ssid":"要连接的wifi名称",
"password":"要连接的wifi密码",
'aio_username' : 'adafruit io账号',# 暂时用不上
'aio_key' : 'adafruit io API key',# 暂时用不上
'weather_key': 'StmLNJLbxIN0acv7f', #心知天气api key
'timezone' : "America/New_York", # http://worldtimeapi.org/timezones
}
以下是code.py文件完整代码:
#adafruit_httpserver/
#adafruit_display_text/
#neopixel.mpy
#adafruit_io/
#adafruit_minimqtt/
import board
import displayio
import time
import ipaddress
import wifi
import ssl
import socketpool
import neopixel
import digitalio
import json
import terminalio
import bitmaptools
#import adafruit_minimqtt.adafruit_minimqtt as MQTT
#from adafruit_io.adafruit_io import IO_MQTT
from adafruit_httpserver import Server, Request, Response, FileResponse, JSONResponse, Redirect, Route, Websocket, Headers
import adafruit_httpserver.methods as METHODS
from adafruit_display_text import label
PORT = 80
WIDTH = board.DISPLAY.width;
HEIGHT = board.DISPLAY.height;
try:
from secrets import secrets
except ImportError:
print("need secrets.py, please add them!")
raise
print("wifi conneting...", secrets["ssid"])
wifi.radio.connect(secrets["ssid"],secrets["password"])
pool=socketpool.SocketPool(wifi.radio)
#=======================================
#mqtt太拉啦,耗时太长,注释掉
#sslContext=ssl.create_default_context()
#sslContext.check_hostname=False
#aio_username=secrets["aio_username"]
#aio_key=secrets['aio_key']
#mqtt_client=MQTT.MQTT(
# broker="io.adafruit.com",
# username=secrets["aio_username"],
# password=secrets["aio_key"],
# socket_pool=pool,
# ssl_context=sslContext,
#)
#def connected(client):
# print("MQTT connected!")
# client.subscribe("neopixel")
#def message(client, id, rgb):
# print("MQTT recv: {0} {1}".format(id, rgb))
# if id=="neopixel":
# global need2update,ledcolor
# need2update=True
# ledcolor = int(rgb[1:],16)
#io=IO_MQTT(mqtt_client)
#io.on_connect=connected
#io.on_message=message
#=======================================
HOST = str(wifi.radio.ipv4_address)
server=Server(pool, 'mywebsite', debug=True)
server.start(HOST, PORT)
print('Server %s: %d is listening... '%(server.host, server.port))
@server.route('/', METHODS.GET)
def rootRedirect(req):
return Redirect(req, '/index.html', permanent=True)
ws=None
def setLedColor(rgb):
pl[0]=rgb;
led.fill(rgb);
@server.route('/rgb', METHODS.GET)
def upgradeWS(req, *args):
h=Headers()
if 'Sec-WebSocket-Protocol' in req.headers:
h.update({'Sec-WebSocket-Protocol':'json'})
global ws
if ws is not None:
ws.close()
ws=Websocket(req, h)
return ws
led=neopixel.NeoPixel(board.NEOPIXEL, 1)
ledcolor = 0xff0000
btn = digitalio.DigitalInOut(board.BOOT0)
btn.switch_to_input(digitalio.Pull.UP)
bmp=displayio.Bitmap(30,30,1)
pl=displayio.Palette(2)
pl[0]=ledcolor
bmp.fill(1)
bitmaptools.draw_circle(bmp, bmp.width//2, bmp.height//2, bmp.width//2-1, 0)
bitmaptools.boundary_fill(bmp, bmp.width//2, bmp.height//2, 0, 1)
colorMark=displayio.TileGrid(bmp, pixel_shader=pl)
colorMark.x=(WIDTH-bmp.width)//2
colorMark.y=(HEIGHT - bmp.height)//2
lb0=label.Label(terminalio.FONT, text='', color=0xffffff)
lb0.y=5
lb0.x=5
gp=displayio.Group()
gp.append(colorMark)
gp.append(lb0)
#不知为何直接在CIRCUITPYTHON_TERMINAL搞会老是重启。
#displayio.CIRCUITPYTHON_TERMINAL.append(colorMark)
board.DISPLAY.root_group = gp
def switchDisplay():
board.DISPLAY.root_group = gp if board.DISPLAY.root_group==displayio.CIRCUITPYTHON_TERMINAL else displayio.CIRCUITPYTHON_TERMINAL
st=time.monotonic()
fps=0
iorate=0
need2update=True
btnv0 = btn.value
btnv1 = btnv0
while True:
server.poll()
if ws is not None:
try:
if (data:=ws.receive()) is not None:
try:
data = json.loads(data)
if(g:=data.get('get')) is not None:
ws.send_message(json.dumps({'rgb':ledcolor}))
elif(rgb:=data.get('rgb')) is not None:
#print(rgb)
if type(rgb)==int:
need2update = True
ledcolor=rgb
except:
pass
except:
ws.close()
ws=None
btnv1=btn.value
et=time.monotonic()
dt = et-st
st=et
fps-=dt
iorate-=dt
#30帧更新频率, 60帧有点快,偶尔卡一下
if(fps<0):
fps=1/30
if need2update:
need2update=False
setLedColor(ledcolor)
if btnv0==True and btnv1==False:
switchDisplay()
btnv0=btnv1
#=======================================
#mqtt太拉啦,耗时太长,注释掉
#if(iorate<0):
# iorate=1
# try:
# if not io.is_connected:
# print("MQTT connecting...")
# io.connect()
# t0=time.monotonic()
# io.loop(0.1)
# print((time.monotonic()-t0)*1000)
# except Exception as e:
# print("MQTT error: ",e)
#=======================================
lb0.text = '%.2f ms'%(dt*1000)
#time.sleep(0.001)
前面几个贴子讲过的就不重复讲了。
源码里面有一大片注释掉的地方,原本是打算把MQTT的方式也用上的,minimqtt模块试了下,
效果并不好,io.loop()这一句至少耗时1秒,太慢了,不符合我的要求,所以不用了。
(mqtt还要去https://io.adafruit.com/建个dashboard,本质上其实也是类似建了个个人网页。)
#用httpserver模块建个web服务器,mywebsite是服务器根目录,随后的文件访问都会被自动改到在该目录下进行。
server=Server(pool, 'mywebsite', debug=True)
server.start(HOST, PORT)
#添加”/“的访问响应,重定向到直接读取index.html。客户端浏览器会自动访问index.html。
(感觉python的发展越来越怪了,@字符一开始看得有点谜)
@server.route('/', METHODS.GET)
def rootRedirect(req):
return Redirect(req, '/index.html', permanent=True)
#在添加个‘/rgb’的响应,把普通http请求的socket升级成websocket,
请求头如果带了Sec-WebSocket-Protocol,需要回复一下,不然升级不了。当然也可以在客户端发请求时不带这个请求头,就不用回复了
只运行一个websocket连接,所以后面加了些判断是否已经存在连接,如果是,就关掉原来的连接。
@server.route('/rgb', METHODS.GET)
def upgradeWS(req, *args):
h=Headers()
if 'Sec-WebSocket-Protocol' in req.headers:
h.update({'Sec-WebSocket-Protocol':'json'})
global ws
if ws is not None:
ws.close()
ws=Websocket(req, h)
return ws
中间一堆到while循环那里,就是一些led设置啊,按键设置啊,屏幕显示啊,就不说了。
#服务器轮询,websocket也是轮询,websocket收到数据就解析成json,先判断是否有‘get’请求,有就回复当前led颜色,没有就提取rgb,存入ledcolor全局变量,
没直接更新led,因为直接更新led会耗时间,websocket请求太快的话,circuitpython好像收到一帧信息不立即返回,而是卡住收一段时间,效果不是很好,所以
我设了个30帧的更新频率,这样客户端快速操作时,esp32也可以快速响应。
while True:
server.poll()
if ws is not None:
try:
if (data:=ws.receive()) is not None:
try:
data = json.loads(data)
if(g:=data.get('get')) is not None:
ws.send_message(json.dumps({'rgb':ledcolor}))
elif(rgb:=data.get('rgb')) is not None:
#print(rgb)
if type(rgb)==int:
need2update = True
ledcolor=rgb
except:
pass
except:
ws.close()
ws=None
......
et=time.monotonic()
dt = et-st
st=et
fps-=dt
#30帧更新频率, 60帧有点快,偶尔卡一下
if(fps<0):
fps=1/30
if need2update:
need2update=False
setLedColor(ledcolor)
if btnv0==True and btnv1==False:
switchDisplay()
btnv0=btnv1
......
以下是index.js文件完整代码:
"use strict";
window.addEventListener('DOMContentLoaded', () => {
const origin = top.location.origin; // http://127.0.0.1
const host = top.location.host; // 127.0.0.1
const hostp = top.location.origin.substr(4); // ://127.0.0.1或者s://127.0.0.1
update();
});
/**@type{HTMLCanvasElement} */
const cv = document.getElementById('cv');
/**@type{CanvasRenderingContext2D} */
const ctx = cv.getContext('2d');
let fps = 0;
let selectedHSL = [0, 100, 50];
let selectedHSLLast = [0, 100, 50];
let selectedHSLInd = 0;
let rgbcolor = 0xff0000;
let paletteOX = cv.width * 0.5;
let paletteOY = cv.height * 0.5;
let paletteRadius = 80;
let paletteWidth = 20;
let paletteGap = 10;
let need2Send = false;
const paletteAngPerSat = 3;
const paletteAngPerLight = 3;
const ws = new WebSocket(`ws://${location.host}/rgb`, 'json');
ws.onmessage = (/**@type{MessageEvent} */ev) => {
const json = JSON.parse(ev.data);
console.log(json)
let r = json.rgb >> 16;
let g = (json.rgb >> 8) & 0x0000ff;
let b = json.rgb & 0x0000ff;
let [h, s, l] = rgb2hsl(r, g, b);
console.log(h, s, l);
selectedHSL[0] = h;
selectedHSL[1] = s;
selectedHSL[2] = l;
rgbcolor = (r << 16) + (g << 8) + (b);
}
ws.onerror = (/**@type{Event} */ev) => {
console.error('ws error: ', ev);
}
ws.onopen = (/**@type{Event} */ev) => {
console.log('ws opened!', ev);
ws.send(JSON.stringify({ get: true }));
}
ws.onclose = (/**@type{CloseEvent} */ev) => {
console.log('ws closed!', ev);
alert("websocket已经断开,请刷新网页重新连接!");
}
/**
* @param {Number} rgb
*/
function sendRGB(rgb) {
ws.send(JSON.stringify({ rgb: rgb }));
}
function resizeCV() {
cv.width = cv.clientWidth;
cv.height = cv.clientHeight;
paletteOX = cv.width * 0.5;
paletteOY = cv.height * 0.5;
}
window.onresize = resizeCV;
cv.addEventListener("mousedown", handleDown);
cv.addEventListener("touchstart", handleDown);
cv.addEventListener("mouseup", handleUp);
cv.addEventListener("touchend", handleUp);
cv.addEventListener("click", handleClick);
cv.addEventListener("mousemove", handleDrag);
cv.addEventListener("touchmove", handleDrag);
resizeCV();
function updateSelectHSLInd(x, y) {
const r = Math.sqrt(x * x + y * y);
if (r < paletteRadius + paletteWidth) {
selectedHSLInd = 0;
} else if (r < paletteRadius + paletteWidth * 2 + paletteGap * 2) {
selectedHSLInd = 1;
} else if (r < paletteRadius + paletteWidth * 3 + paletteGap * 3) {
selectedHSLInd = 2
}
}
function handleDown(/**@type{TouchEvent | MouseEvent} */ev) {
let x = 0;
let y = 0;
if (ev instanceof MouseEvent) {
x = ev.clientX;
y = ev.clientY;
} else {
x = ev.touches[0].clientX;
y = ev.touches[0].clientY;
}
x = x - paletteOX;
y = - y + paletteOY;
updateSelectHSLInd(x, y);
}
function handleClick(/**@type{TouchEvent | MouseEvent} */ev) {
// console.log(ev.button);
}
function handleUp(/**@type{TouchEvent | MouseEvent} */ev) {
selectedHSLLast[0] = selectedHSL[0];
selectedHSLLast[1] = selectedHSL[1];
selectedHSLLast[2] = selectedHSL[2];
}
function handleDrag(/**@type{TouchEvent | MouseEvent} */ev) {
ev.preventDefault();
let x = 0;
let y = 0;
if (ev instanceof MouseEvent) {
if (!ev.buttons) return;
x = ev.clientX;
y = ev.clientY;
} else {
x = ev.touches[0].clientX;
y = ev.touches[0].clientY;
}
// console.log(ev);
x = x - paletteOX;
y = - y + paletteOY;
updateSelectHSLInd(x, y);
//-180 ~ 180
let deg = 0;
let a, cosa, sina, x1, y1, maxdeg;
switch (selectedHSLInd) {
case 0:
deg = Math.floor(Math.atan2(y, x) * 180 / Math.PI)
deg = deg < 0 ? 360 + deg : deg;
break;
case 1:
a = deg2rad(selectedHSL[0]);
cosa = Math.cos(a);
sina = Math.sin(a);
x1 = cosa * x + sina * y;
y1 = cosa * y - sina * x;
deg = -Math.floor(Math.atan2(y1, x1) * 180 / Math.PI)
deg = deg < 0 ? 360 + deg : deg;
maxdeg = paletteAngPerSat * 100;
if (deg >= 0 && deg <= maxdeg) {
}
else if (deg < 360 - (360 - maxdeg) * 0.5) {
deg = maxdeg;
}
else {
deg = 0;
}
deg = 100 - Math.floor(deg / paletteAngPerSat);
break;
default:
a = deg2rad(selectedHSL[0] - (100 - selectedHSL[1]) * paletteAngPerSat - 50 * paletteAngPerLight);
cosa = Math.cos(a);
sina = Math.sin(a);
x1 = cosa * x + sina * y;
y1 = cosa * y - sina * x;
deg = Math.floor(Math.atan2(y1, x1) * 180 / Math.PI)
deg = deg < 0 ? 360 + deg : deg;
maxdeg = paletteAngPerLight * 100;
if (deg >= 0 && deg <= maxdeg) {
}
else if (deg < 360 - (360 - maxdeg) * 0.5) {
deg = maxdeg;
}
else {
deg = 0;
}
deg = Math.floor(deg / paletteAngPerLight);
break;
}
selectedHSL[selectedHSLInd] = deg;
let [r, g, b] = hsl2rgbInt(selectedHSL[0], selectedHSL[1], selectedHSL[2]);
rgbcolor = (r << 16) + (g << 8) + (b);
need2Send = true;
}
function clear() {
ctx.clearRect(0, 0, cv.width, cv.height)
}
function draw() {
clear();
drawPalette(ctx, paletteOX, paletteOY, paletteRadius, paletteWidth, paletteGap, ...selectedHSL, ...selectedHSLLast, selectedHSLInd);
ctx.font = '24px sans-serif';
ctx.fillStyle = 'black';
let s = `HSL: ${selectedHSL[0]} ${selectedHSL[1]}% ${selectedHSL[2]}%`;
let m = ctx.measureText(s);
ctx.fillText(s, (cv.width - m.width) * 0.5, cv.height * 0.5 + paletteRadius + paletteWidth * 3 + paletteGap * 3 + 24);
let r = rgbcolor >> 16;
let g = (rgbcolor >> 8) & 0x0000ff;
let b = (rgbcolor) & 0x0000ff;
function to16(num) {
num = num.toString(16).toUpperCase();
return num.length < 2 ? '0' + num : num;
}
let srgb = `0x${to16(r)}${to16(g)}${to16(b)}`;
s = `RGB: ${r} ${g} ${b} ${srgb}`;
m = ctx.measureText(s);
ctx.fillText(s, (cv.width - m.width) * 0.5, cv.height * 0.5 + paletteRadius + paletteWidth * 3 + paletteGap * 3 + 48);
drawFPS(ctx);
}
function drawFPS(/**@type{CanvasRenderingContext2D} */ctx) {
ctx.font = "32px sans-serif";
ctx.fillText(`FPS: ${fps.toFixed(2)}`, 5, 40);
}
/**
* 圆上的点
* @param {Number} ox
* @param {Number} oy
* @param {Number} r
* @param {Number} rad
* @returns {[Number, Number]} [x, y]
*/
function getPointOnCircle(ox, oy, r, rad) {
return [ox + (r) * Math.cos(rad), oy - (r) * Math.sin(rad)];
}
function drawPalette(/**@type{CanvasRenderingContext2D} */ctx, x, y, r, w, paletteGap, hue, sat, light, hue0, sat0, light0, markInd) {
let rad = deg2rad(0);
let rr = r;
/**hue */
for (let i = 0; i < 360; i++) {
ctx.beginPath();
ctx.moveTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
rad = deg2rad(i + 1);
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.closePath();
ctx.fillStyle = `hsl(${i}, 100%, 50%)`;
ctx.fill();
}
/**sat */
let angOffset = hue;
rr = rr + w + paletteGap;
rad = deg2rad(angOffset);
ctx.strokeStyle = 'white';
ctx.lineWidth = 2;
for (let i = 0; i < 101; i++) {
ctx.beginPath();
ctx.moveTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
rad = deg2rad(angOffset - i * paletteAngPerSat);
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.closePath();
ctx.stroke();
ctx.fillStyle = `hsl(${hue}, ${100 - i}%, 50%)`;
ctx.fill();
}
/**light */
rr = rr + w + paletteGap;
angOffset = hue - (100 - sat) * paletteAngPerSat;
rad = deg2rad(angOffset + 50 * paletteAngPerLight);
ctx.lineWidth = 3;
for (let i = -50; i < 51; i++) {
ctx.beginPath();
ctx.moveTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
rad = deg2rad(angOffset - i * paletteAngPerLight);
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w, rad));
ctx.closePath();
ctx.stroke();
ctx.fillStyle = `hsl(${hue}, ${sat}%, ${50 - i}%)`;
ctx.fill();
}
/**center */
ctx.beginPath();
ctx.moveTo(x - 1, y);
ctx.arc(x - 1, y, r - w, Math.PI * 0.5, Math.PI * 1.5, false);
ctx.fillStyle = `hsl(${hue0}, ${sat0}%, ${light0}%)`
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(x + 1, y);
ctx.arc(x + 1, y, r - w, Math.PI * 0.5, Math.PI * 1.5, true);
ctx.fillStyle = `hsl(${hue}, ${sat}%, ${light}%)`
ctx.closePath();
ctx.fill();
/**mark */
let color = '';
let s = '';
let marka = 1;
let markw = 6;
switch (markInd) {
case 0:
angOffset = hue;
color = `hsl(${hue}, 100%, 50%)`;
s = `${hue}`;
break;
case 1:
angOffset = hue - (100 - sat) * paletteAngPerSat;
color = `hsl(${hue}, ${sat}%, 50%)`;
s = `${sat}`;
marka = -paletteAngPerSat;
markw = -6;
break;
default:
angOffset = hue - (100 - sat) * paletteAngPerSat - (50 - light) * paletteAngPerLight;
color = `hsl(${hue}, ${sat}%, ${light}%)`;
s = `${light}`;
marka = paletteAngPerLight;
markw = 6;
break;
}
rad = deg2rad(angOffset);
rr = r + (markInd) * (w + paletteGap)
const gap = 2;
ctx.beginPath();
ctx.moveTo(...getPointOnCircle(x, y, rr, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w + gap, rad));
rad = deg2rad(angOffset - markw);
ctx.lineTo(...getPointOnCircle(x, y, rr + w + gap, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w + w, rad));
rad = deg2rad(angOffset + markw + marka);
ctx.lineTo(...getPointOnCircle(x, y, rr + w + w, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr + w + gap, rad));
rad = deg2rad(angOffset + marka);
ctx.lineTo(...getPointOnCircle(x, y, rr + w + gap, rad));
ctx.lineTo(...getPointOnCircle(x, y, rr, rad));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
let l = markInd == 2 ? (light < 30 ? 100 : 0) : markInd == 1 ? (0) : 50;
let c = `hsl(${180 + hue}, ${(sat)}%, ${(l)}%)`;
ctx.strokeStyle = c;
ctx.lineWidth = 1;
ctx.stroke();
ctx.save();
ctx.fillStyle = c;
ctx.font = "14px sans-serif";
const m = ctx.measureText(s);
ctx.translate(x, y)
ctx.rotate(0.5 * Math.PI - deg2rad(angOffset + marka * 0.5));
ctx.translate(0, -rr - w - gap - 2)
ctx.fillText(s, - m.width * 0.5, 0);
ctx.restore();
}
let st = new Date().getTime();
function update() {
let et = new Date().getTime();
fps = 1000 / (et - st);
// console.log('FPS: ', fps);
st = et;
draw();
if (need2Send) {
sendRGB(rgbcolor);
need2Send = false;
}
requestAnimationFrame(update);
}
/**
*
* @param {Number} h 0 ~ 360
* @param {Number} s 0.0 ~ 1.0
* @param {Number} l 0.0 ~ 1.0
* @returns {[Number, Number, Number]} RGB: 0.0 ~ 1.0
*/
function hsl2rgb(h, s, l) {
let r = 0, g = 0, b = 0;
if (s === 0) {
r = g = b = l;
} else {
const q = l < 0.5 ? l * (1 + s) : l + s - (l * s);
const p = 2 * l - q;
const k = h / 360;
const f = function (p, q, t) {
if (t < 0) {
t += 1;
} else if (t > 1) {
t -= 1;
}
let c = 0;
if (t < 1 / 6) {
c = p + ((q - p) * 6 * t);
} else if (t < 1 / 2) {
c = q;
} else if (t < 2 / 3) {
c = p + ((q - p) * 6 * (2 / 3 - t));
} else {
c = p;
}
return c;
}
r = f(p, q, k + 1 / 3);
g = f(p, q, k);
b = f(p, q, k - 1 / 3);
}
return [r, g, b];
}
/**
* @param {Number} h 0~360
* @param {Number} s 0~100
* @param {Nubmer} l 0~100
* @returns {[Number, Number, Number]} [255, 255, 255]
*/
function hsl2rgbInt(h, s, l) {
let [r, g, b] = hsl2rgb(h, s / 100, l / 100);
r *= 255;
g *= 255;
b *= 255;
r = Math.round(r);
g = Math.round(g);
b = Math.round(b);
return [r, g, b];
}
/**
* @param {Number} r 0~255
* @param {Number} g 0~255
* @param {Number} b 0~255
* @returns {[Number, Number, Number]} [0~360, 0~100, 0~100]
*/
function rgb2hsl(r, g, b) {
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
if (max === min) {
h = 0;
} else if (max === r) {
h = 60 * (g - b) / (max - min);
if (g < b)
h += 360;
} else if (max === g) {
h = 60 * (b - r) / (max - min) + 120
} else if (max === b) {
h = 60 * (r - g) / (max - min) + 240
}
let l = 0.5 * (max + min) / 255 * 100;
let s = 0;
if (l === 0 || max === min) {
s = 0;
} else if (l <= 50) {
s = (max - min) / (max + min) * 100;
} else {
s = (max - min) / (510 - (max + min)) * 100;
}
return [Math.round(h), Math.round(s), Math.round(l)]
}
function deg2rad(deg) {
return Math.PI * deg / 180;
}
js源码我就不说了,其实花费时间最多的就是这个js文件了,一个狂拽酷炫吊炸天的调色盘,花费了我无数个夜晚,
不过转起来不是那么顺风顺水,一不留神就转到其他圆环了。
一开始是点一下指定圆环,然后只控制那个环,但是转起来不够狂,于是改成指哪转哪,但是还是不好,算了不改了,
你们要是喜欢就拿去吧。
吐槽一下这个任务居然要手机控制,只有一个手机的我费了九牛二虎之力才找到了解决办法:
需要用到的网站https://vdo.ninja/ 可以自己开直播哈哈哈。
效果展示:
|