Sorry for being a pain, I must be annoying you by now, I’m just finding it hard to wrap my head around
Thank you i’ll give these a good read, I wish the connector would just hurry up and get here so I can test it physically best way to learn I think
Nice.
Feel like sharing the html or seeing if you can integrate it here?
Can anybody help me confirm something? The cylon style when active in the blade style editor shows the beam going from the base of the blade to the top of the blade and back-and-forth. How does this look on a PCB? Would it be for instance going from 12 o’clock clockwise to 12 o’clock and then reverse anticlockwise back to 12 o’clock and then repeat?
ive updated this code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pixel LED PCB Style Builder</title>
<style>
body {
background: #111;
color: #fff;
font-family: sans-serif;
text-align: center;
}
canvas {
display: block;
margin: 20px auto;
background: #222;
border: 4px solid #444;
border-radius: 50%;
box-shadow: 0 0 20px #000 inset;
}
.controls {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
select, input[type="color"] {
font-size: 16px;
padding: 5px;
}
#exportOutput {
white-space: pre-wrap;
padding: 10px;
color: #0f0;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Pixel LED PCB Style Builder</h1>
<canvas id="bladeCanvas" width="400" height="400"></canvas>
<div class="controls">
<label>Pixel Mode:
<select id="pixelMode" onchange="setPixelMode()">
<option value="16">16 Pixel</option>
<option value="5">5 Pixel</option>
</select>
</label>
<label>Outer Effect:
<select id="outerEffect">
<option value="none">None</option>
<option value="solid">Solid</option>
<option value="colorcycle">ColorCycle</option>
<option value="pulsing">Pulsing</option>
<option value="humpflicker">HumpFlicker</option>
<option value="stripes">Stripes</option>
<option value="cylon">Cylon</option>
<option value="strobe">Strobe</option>
</select>
</label>
<label>Outer Color:
<input type="color" id="outerColor" value="#00ffff">
</label>
<label>Inner Effect:
<select id="innerEffect">
<option value="none">None</option>
<option value="solid">Solid</option>
<option value="colorcycle">ColorCycle</option>
<option value="pulsing">Pulsing</option>
<option value="humpflicker">HumpFlicker</option>
<option value="stripes">Stripes</option>
<option value="cylon">Cylon</option>
<option value="strobe">Strobe</option>
</select>
</label>
<label>Inner Color:
<input type="color" id="innerColor" value="#ff0000">
</label>
<label>Outer Speed:
<input type="range" id="outerSpeed" min="1" max="100" value="80" oninput="updateSpeedDisplay()">
<span id="outerSpeedValue">80</span>
</label>
<label>Inner Speed:
<input type="range" id="innerSpeed" min="1" max="100" value="80" oninput="updateSpeedDisplay()">
<span id="innerSpeedValue">80</span>
</label>
<button onclick="exportStyle()">Export Proffie Code</button>
<pre id="exportOutput"></pre>
</div>
<script>
const canvas = document.getElementById("bladeCanvas");
const ctx = canvas.getContext("2d");
let pixelLayout = { outer: 8, inner: 8 };
let t = 0;
let currentMode = "16";
// 8 outer and 8 inner, each in their own ring and rotated outward
const ROTATION_OFFSET_DEG = 22;
const ROTATION_OFFSET_RAD = ROTATION_OFFSET_DEG * Math.PI / 180;
const NPXL_POSITIONS = [
// Outer ring (radius 175, close to edge)
...Array.from({length: 8}).map((_, i) => {
const angle = (2 * Math.PI * i) / 8 - Math.PI / 2 + ROTATION_OFFSET_RAD;
const cx = 200, cy = 200;
const radius = 165;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
return { x, y, rotation };
}),
// Inner ring (radius 115)
...Array.from({length: 8}).map((_, i) => {
const angle = (2 * Math.PI * i) / 8 - Math.PI / 2 + ROTATION_OFFSET_RAD;
const cx = 200, cy = 200;
const radius = 105;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
return { x, y, rotation };
})
];
function setPixelMode() {
currentMode = document.getElementById("pixelMode").value;
if (currentMode === "5") {
pixelLayout = { outer: 5, inner: 0 };
} else {
pixelLayout = { outer: 8, inner: 8 };
}
}
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
}
function updateSpeedDisplay() {
document.getElementById("outerSpeedValue").textContent = document.getElementById("outerSpeed").value;
document.getElementById("innerSpeedValue").textContent = document.getElementById("innerSpeed").value;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
t++;
const outerEffect = document.getElementById("outerEffect").value;
const outerColor = hexToRgb(document.getElementById("outerColor").value);
const outerSpeed = 101 - parseInt(document.getElementById("outerSpeed").value);
const innerEffect = document.getElementById("innerEffect").value;
const innerColor = hexToRgb(document.getElementById("innerColor").value);
const innerSpeed = 101 - parseInt(document.getElementById("innerSpeed").value);
const size = 50;
if (currentMode === "16") {
for (let i = 0; i < 16; i++) {
const isOuter = i < 8;
const effect = isOuter ? outerEffect : innerEffect;
const col = isOuter ? outerColor : innerColor;
const speed = isOuter ? outerSpeed : innerSpeed;
let color = `rgb(0, 0, 0)`;
const p = 0.5 + 0.5 * Math.sin(t / (speed * 2));
if (effect === "colorcycle") {
const cylonPos = Math.floor((t / speed) % 8);
color = ((i % 8) === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "cylon") {
const loopDuration = 14;
const frame = Math.floor((t / speed) % loopDuration);
const cylonPos = frame < 8 ? frame : loopDuration - frame;
color = ((i % 8) === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "solid") {
color = `rgb(${col[0]}, ${col[1]}, ${col[2]})`;
} else if (effect === "pulsing") {
color = `rgb(${Math.floor(col[0] * p)}, ${Math.floor(col[1] * p)}, ${Math.floor(col[2] * p)})`;
} else if (effect === "humpflicker") {
const hump = Math.sin((t + i * 10) / speed);
color = `rgb(${Math.floor(col[0] * hump * hump)}, ${Math.floor(col[1] * hump * hump)}, ${Math.floor(col[2] * hump * hump)})`;
} else if (effect === "stripes") {
color = ((i + Math.floor(t / speed)) % 3 === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "strobe") {
color = ((t % 4) === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
}
const {x, y, rotation} = NPXL_POSITIONS[i];
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation || 0) * Math.PI / 180);
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.fillRect(-size / 2, -size / 2, size, size);
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation || 0) * Math.PI / 180);
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.strokeRect(-size / 2, -size / 2, size, size);
ctx.restore();
}
} else {
// 5-pixel mode: evenly distribute 5 pixels in the outer ring, facing outward, rotated by 50 deg extra
const radius = 140;
const FIVE_PIXEL_ROTATION_DEG = 50;
const FIVE_PIXEL_ROTATION_RAD = FIVE_PIXEL_ROTATION_DEG * Math.PI / 180;
const size = 72; // Larger pixels in 5-pixel mode!
for (let i = 0; i < 5; i++) {
const angle = (2 * Math.PI * i) / 5 - Math.PI / 2 + ROTATION_OFFSET_RAD + FIVE_PIXEL_ROTATION_RAD;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
const effect = outerEffect;
const col = outerColor;
const speed = outerSpeed;
let color = `rgb(0, 0, 0)`;
const p = 0.5 + 0.5 * Math.sin(t / (speed * 2));
if (effect === "colorcycle") {
const cylonPos = Math.floor((t / speed) % 5);
color = (i === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "cylon") {
const loopDuration = 8;
const frame = Math.floor((t / speed) % loopDuration);
const cylonPos = frame < 5 ? frame : loopDuration - frame;
color = (i === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "solid") {
color = `rgb(${col[0]}, ${col[1]}, ${col[2]})`;
} else if (effect === "pulsing") {
color = `rgb(${Math.floor(col[0] * p)}, ${Math.floor(col[1] * p)}, ${Math.floor(col[2] * p)})`;
} else if (effect === "humpflicker") {
const hump = Math.sin((t + i * 10) / speed);
color = `rgb(${Math.floor(col[0] * hump * hump)}, ${Math.floor(col[1] * hump * hump)}, ${Math.floor(col[2] * hump * hump)})`;
} else if (effect === "stripes") {
color = ((i + Math.floor(t / speed)) % 3 === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "strobe") {
color = ((t % 4) === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
}
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * Math.PI / 180);
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.fillRect(-size / 2, -size / 2, size, size);
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * Math.PI / 180);
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.strokeRect(-size / 2, -size / 2, size, size);
ctx.restore();
}
}
requestAnimationFrame(draw);
}
function exportStyle() {
const outerEffect = document.getElementById("outerEffect").value;
const outerColor = document.getElementById("outerColor").value;
const innerEffect = document.getElementById("innerEffect").value;
const innerColor = document.getElementById("innerColor").value;
function rgbText(hex) {
const r = parseInt(hex.substr(1,2), 16);
const g = parseInt(hex.substr(3,2), 16);
const b = parseInt(hex.substr(5,2), 16);
return `Rgb<${r},${g},${b}>`;
}
function effectCode(effect, color) {
switch (effect) {
case "colorcycle": return `ColorCycle<${color},20,50,${color},20,50,1000>`;
case "pulsing": return `Pulsing<Black,${color},2000>`;
case "humpflicker": return `HumpFlicker<${color},Black,100>`;
case "stripes": return `Stripes<10,-700,${color},Black,Black>`;
case "cylon": return `Cylon<${color},6,20>`;
case "strobe": return `Strobe<Black,${color},15,1>`;
case "solid": return `${color}`;
default: return `Black`;
}
}
let code = "";
if (currentMode === "16") {
const innerCode = effectCode(innerEffect, rgbText(innerColor));
const outerCode = effectCode(outerEffect, rgbText(outerColor));
const codes = [];
// Correct order: outer, inner, outer, inner, ...
for (let i = 0; i < 8; i++) {
codes.push(outerCode); // Outer
codes.push(innerCode); // Inner
}
code = `StylePtr<Gradient<${codes.join(",")}>>(),`;
} else {
// 5-pixel mode, all outer only
const outerCode = effectCode(outerEffect, rgbText(outerColor));
const codes = [];
for (let i = 0; i < 5; i++) codes.push(outerCode);
code = `StylePtr<Gradient<${codes.join(",")}>>(),`;
}
document.getElementById("exportOutput").textContent = code;
}
updateSpeedDisplay();
draw();
</script>
</body>
</html>
I’m working on a version inside Style Editor
Meanwhile, check this out
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pixel LED PCB Style Builder</title>
<style>
body {
background: #111;
color: #fff;
font-family: sans-serif;
text-align: center;
}
canvas {
display: block;
margin: 20px auto;
background: #222;
border: 4px solid #444;
border-radius: 50%;
box-shadow: 0 0 20px #000 inset;
}
.controls-row {
display: flex;
flex-direction: row;
gap: 40px;
justify-content: center;
align-items: flex-start;
margin-bottom: 16px;
}
.section-group {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
background: #181818;
padding: 18px 28px;
border-radius: 18px;
box-shadow: 0 2px 10px #0004;
min-width: 220px;
}
.controls-bottom {
display: flex;
justify-content: center;
gap: 18px;
margin-top: 12px;
}
.inline {
display: inline-block;
margin-left: 8px;
}
select, input[type="color"] {
font-size: 16px;
padding: 5px;
}
#exportOutput {
white-space: pre-wrap;
padding: 10px;
color: #0f0;
margin-top: 10px;
}
</style>
</head>
<body>
<h1>Pixel LED PCB Style Builder</h1>
<canvas id="bladeCanvas" width="400" height="400"></canvas>
<div style="text-align: center; margin-bottom: 14px;">
<label style="font-size: 17px;">
Pixel Mode:
<select id="pixelMode" onchange="setPixelMode()">
<option value="16">16 Pixel</option>
<option value="5">5 Pixel</option>
</select>
</label>
</div>
<!-- Outer/Inner controls side by side -->
<div class="controls-row">
<div class="section-group" id="outerSection">
<label>Outer Effect:
<select id="outerEffect">
<option value="none">None</option>
<option value="solid">Solid</option>
<option value="colorcycle">ColorCycle</option>
<option value="pulsing">Pulsing</option>
<option value="humpflicker">HumpFlicker</option>
<option value="stripes">Stripes</option>
<option value="cylon">Cylon</option>
<option value="strobe">Strobe</option>
<option value="runsegment">Run Segment</option>
</select>
</label>
<label>Outer Color:
<input type="color" id="outerColor" value="#00ffff">
</label>
<label>Outer Speed:
<input type="range" id="outerSpeed" min="1" max="100" value="80" oninput="updateSpeedDisplay()">
<span id="outerSpeedValue">80</span>
</label>
<label>Outer Direction:
<select id="outerDirection">
<option value="forward">Forward</option>
<option value="reverse">Reverse</option>
</select>
</label>
<label id="outerRunLenLabel" style="display:none;">
Run Length:
<select id="outerRunLen">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</label>
</div>
<div class="section-group" id="innerOptions">
<label>Inner Effect:
<select id="innerEffect">
<option value="none">None</option>
<option value="solid">Solid</option>
<option value="colorcycle">ColorCycle</option>
<option value="pulsing">Pulsing</option>
<option value="humpflicker">HumpFlicker</option>
<option value="stripes">Stripes</option>
<option value="cylon">Cylon</option>
<option value="strobe">Strobe</option>
<option value="runsegment">Run Segment</option>
</select>
</label>
<label>Inner Color:
<input type="color" id="innerColor" value="#ff0000">
</label>
<label>Inner Speed:
<input type="range" id="innerSpeed" min="1" max="100" value="80" oninput="updateSpeedDisplay()">
<span id="innerSpeedValue">80</span>
</label>
<label>Inner Direction:
<select id="innerDirection">
<option value="forward">Forward</option>
<option value="reverse">Reverse</option>
</select>
</label>
<label id="innerRunLenLabel" style="display:none;">
Run Length:
<select id="innerRunLen">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</label>
</div>
</div>
<div class="controls-bottom">
<button onclick="exportStyle()">Generate Proffie Code</button>
<button id="copyCodeBtn" style="display:none;">Copy Generated Code</button>
<span id="copyStatus" style="color:#0f0; margin-left:8px;"></span>
<!-- <pre id="exportOutput"></pre> -->
</div>
<script>
const canvas = document.getElementById("bladeCanvas");
const ctx = canvas.getContext("2d");
let pixelLayout = { outer: 8, inner: 8 };
let t = 0;
let currentMode = "16";
// 8 outer and 8 inner, each in their own ring and rotated outward
const ROTATION_OFFSET_DEG = 22;
const ROTATION_OFFSET_RAD = ROTATION_OFFSET_DEG * Math.PI / 180;
const NPXL_POSITIONS = [
// Outer ring (radius 175, close to edge)
...Array.from({length: 8}).map((_, i) => {
const angle = (2 * Math.PI * i) / 8 - Math.PI / 2 + ROTATION_OFFSET_RAD;
const cx = 200, cy = 200;
const radius = 165;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
return { x, y, rotation };
}),
// Inner ring (radius 115)
...Array.from({length: 8}).map((_, i) => {
const angle = (2 * Math.PI * i) / 8 - Math.PI / 2 + ROTATION_OFFSET_RAD;
const cx = 200, cy = 200;
const radius = 105;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
return { x, y, rotation };
})
];
function setPixelMode() {
currentMode = document.getElementById("pixelMode").value;
if (currentMode === "5") {
pixelLayout = { outer: 5, inner: 0 };
document.getElementById("innerOptions").style.display = "none";
} else {
pixelLayout = { outer: 8, inner: 8 };
document.getElementById("innerOptions").style.display = "";
}
// Update runsegment bar length dropdowns in case pixel count changed
updateOuterRunsegmentUI();
updateInnerRunsegmentUI();
}
// --- Runsegment bar length dropdown logic ---
function updateOuterRunsegmentUI() {
const outerEffect = document.getElementById("outerEffect").value;
const lenSel = document.getElementById("outerRunLen");
// How many options based on mode?
const count = (currentMode === "16") ? 8 : 5;
if (outerEffect === "runsegment") {
lenSel.style.display = "";
// Populate only if needed
if (lenSel.options.length !== count) {
lenSel.innerHTML = "";
for (let n = 1; n <= count; n++) {
let opt = document.createElement("option");
opt.value = n;
opt.textContent = "Segment Length " + n;
lenSel.appendChild(opt);
}
lenSel.value = 2;
}
} else {
lenSel.style.display = "none";
}
}
function updateInnerRunsegmentUI() {
const innerEffect = document.getElementById("innerEffect").value;
const lenSel = document.getElementById("innerRunLen");
// How many options based on mode? Only if mode=16 (8 inner), else hide.
const count = 8;
if (currentMode === "16" && innerEffect === "runsegment") {
lenSel.style.display = "";
if (lenSel.options.length !== count) {
lenSel.innerHTML = "";
for (let n = 1; n <= count; n++) {
let opt = document.createElement("option");
opt.value = n;
opt.textContent = n + " wide";
lenSel.appendChild(opt);
}
lenSel.value = 2;
}
} else {
lenSel.style.display = "none";
}
}
document.getElementById("outerEffect").addEventListener("change", updateOuterRunsegmentUI);
document.getElementById("innerEffect").addEventListener("change", updateInnerRunsegmentUI);
// --- End runsegment bar length dropdown logic ---
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
}
function updateSpeedDisplay() {
document.getElementById("outerSpeedValue").textContent = document.getElementById("outerSpeed").value;
document.getElementById("innerSpeedValue").textContent = document.getElementById("innerSpeed").value;
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const cx = canvas.width / 2;
const cy = canvas.height / 2;
t++;
const outerEffect = document.getElementById("outerEffect").value;
const outerColor = hexToRgb(document.getElementById("outerColor").value);
const outerSpeed = 101 - parseInt(document.getElementById("outerSpeed").value);
const innerEffect = document.getElementById("innerEffect").value;
const innerColor = hexToRgb(document.getElementById("innerColor").value);
const innerSpeed = 101 - parseInt(document.getElementById("innerSpeed").value);
const outerDir = document.getElementById("outerDirection").value;
const innerDir = document.getElementById("innerDirection").value;
const outerBarLen = parseInt(document.getElementById("outerRunLen").value) || 2;
const innerBarLen = parseInt(document.getElementById("innerRunLen").value) || 2;
const size = 50;
if (currentMode === "16") {
for (let i = 0; i < 16; i++) {
const isOuter = i < 8;
const effect = isOuter ? outerEffect : innerEffect;
const col = isOuter ? outerColor : innerColor;
const speed = isOuter ? outerSpeed : innerSpeed;
const barLen = isOuter ? outerBarLen : innerBarLen;
let color = `rgb(0, 0, 0)`;
const p = 0.5 + 0.5 * Math.sin(t / (speed * 2));
const total = 8;
const dir = isOuter ? outerDir : innerDir;
if (effect === "colorcycle") {
let cylonPos = Math.floor((t / speed) % total);
if (dir === "reverse") {
cylonPos = (total - cylonPos) % total;
}
color = ((i % total) === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "stripes") {
let stripePos = Math.floor(t / speed);
if (dir === "reverse") {
stripePos = -stripePos;
}
color = (((i + stripePos) % 3 + 3) % 3 === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "cylon") {
const loopDuration = 14;
const frame = Math.floor((t / speed) % loopDuration);
const cylonPos = frame < 8 ? frame : loopDuration - frame;
color = ((i % 8) === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "solid") {
color = `rgb(${col[0]}, ${col[1]}, ${col[2]})`;
} else if (effect === "pulsing") {
color = `rgb(${Math.floor(col[0] * p)}, ${Math.floor(col[1] * p)}, ${Math.floor(col[2] * p)})`;
} else if (effect === "humpflicker") {
const hump = Math.sin((t + i * 10) / speed);
color = `rgb(${Math.floor(col[0] * hump * hump)}, ${Math.floor(col[1] * hump * hump)}, ${Math.floor(col[2] * hump * hump)})`;
} else if (effect === "strobe") {
// Use speed to control the strobe interval; higher speed = slower strobe
const strobeInterval = Math.max(2, Math.floor(speed * 0.7 + 5));
color = ((Math.floor(t / strobeInterval) % 2) === 0)
? `rgb(${col[0]}, ${col[1]}, ${col[2]})`
: `rgb(0, 0, 0)`;
} else if (effect === "runsegment") {
const wipeDuration = speed;
const total = 8;
let barStart = Math.floor(((t / wipeDuration)) % total);
if (dir === "reverse") {
barStart = (total - barStart - barLen + 1 + total) % total;
}
let on = false;
for (let b = 0; b < barLen; b++) {
if (((i % total) + total) % total === (barStart + b) % total) {
on = true;
break;
}
}
color = on ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0,0,0)`;
}
const {x, y, rotation} = NPXL_POSITIONS[i];
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation || 0) * Math.PI / 180);
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.fillRect(-size / 2, -size / 2, size, size);
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.rotate((rotation || 0) * Math.PI / 180);
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.strokeRect(-size / 2, -size / 2, size, size);
ctx.restore();
}
} else {
// 5-pixel mode: evenly distribute 5 pixels in the outer ring
const radius = 140;
const FIVE_PIXEL_ROTATION_DEG = 50;
const FIVE_PIXEL_ROTATION_RAD = FIVE_PIXEL_ROTATION_DEG * Math.PI / 180;
const size = 72;
for (let i = 0; i < 5; i++) {
const angle = (2 * Math.PI * i) / 5 - Math.PI / 2 + ROTATION_OFFSET_RAD + FIVE_PIXEL_ROTATION_RAD;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
const rotation = (angle * 180 / Math.PI) + 90;
const effect = outerEffect;
const col = outerColor;
const speed = outerSpeed;
const barLen = outerBarLen;
let color = `rgb(0, 0, 0)`;
const p = 0.5 + 0.5 * Math.sin(t / (speed * 2));
if (effect === "colorcycle") {
let cylonPos = Math.floor((t / speed) % 5);
if (outerDir === "reverse") {
cylonPos = (5 - cylonPos) % 5;
}
color = (i === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "stripes") {
let stripePos = Math.floor(t / speed);
if (outerDir === "reverse") {
stripePos = -stripePos;
}
color = (((i + stripePos) % 3 + 3) % 3 === 0) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "cylon") {
const loopDuration = 8;
const frame = Math.floor((t / speed) % loopDuration);
const cylonPos = frame < 5 ? frame : loopDuration - frame;
color = (i === cylonPos) ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0, 0, 0)`;
} else if (effect === "solid") {
color = `rgb(${col[0]}, ${col[1]}, ${col[2]})`;
} else if (effect === "pulsing") {
color = `rgb(${Math.floor(col[0] * p)}, ${Math.floor(col[1] * p)}, ${Math.floor(col[2] * p)})`;
} else if (effect === "humpflicker") {
const hump = Math.sin((t + i * 10) / speed);
color = `rgb(${Math.floor(col[0] * hump * hump)}, ${Math.floor(col[1] * hump * hump)}, ${Math.floor(col[2] * hump * hump)})`;
} else if (effect === "strobe") {
// Use speed to control the strobe interval; higher speed = slower strobe
const strobeInterval = Math.max(2, Math.floor(speed * 0.7 + 5));
color = ((Math.floor(t / strobeInterval) % 2) === 0)
? `rgb(${col[0]}, ${col[1]}, ${col[2]})`
: `rgb(0, 0, 0)`;
} else if (effect === "runsegment") {
const wipeDuration = speed;
const total = 5;
let barStart = Math.floor(((t / wipeDuration)) % total);
if (outerDir === "reverse") {
barStart = (total - barStart - barLen + 1 + total) % total;
}
let on = false;
for (let b = 0; b < barLen; b++) {
if (((i + total) % total) === (barStart + b) % total) {
on = true;
break;
}
}
color = on ? `rgb(${col[0]}, ${col[1]}, ${col[2]})` : `rgb(0,0,0)`;
}
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * Math.PI / 180);
ctx.shadowColor = color;
ctx.shadowBlur = 12;
ctx.fillStyle = color;
ctx.fillRect(-size / 2, -size / 2, size, size);
ctx.restore();
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * Math.PI / 180);
ctx.strokeStyle = "#333";
ctx.lineWidth = 2;
ctx.strokeRect(-size / 2, -size / 2, size, size);
ctx.restore();
}
}
requestAnimationFrame(draw);
}
function exportStyle() {
const outerEffect = document.getElementById("outerEffect").value;
const outerColor = document.getElementById("outerColor").value;
const innerEffect = document.getElementById("innerEffect").value;
const innerColor = document.getElementById("innerColor").value;
const outerBarLen = parseInt(document.getElementById("outerRunLen").value) || 2;
const innerBarLen = parseInt(document.getElementById("innerRunLen").value) || 2;
document.getElementById("copyCodeBtn").onclick = function() {
if (window._proffieGeneratedCode) {
navigator.clipboard.writeText(window._proffieGeneratedCode)
.then(() => {
document.getElementById("copyStatus").textContent = "Copied!";
setTimeout(() => { document.getElementById("copyStatus").textContent = ""; }, 1500);
});
}
};
function rgbText(hex) {
const r = parseInt(hex.substr(1,2), 16);
const g = parseInt(hex.substr(3,2), 16);
const b = parseInt(hex.substr(5,2), 16);
return `Rgb<${r},${g},${b}>`;
}
function effectCode(effect, color, barLen = 2) {
switch (effect) {
case "colorcycle": return `ColorCycle<${color},20,50,${color},20,50,1000>`;
case "pulsing": return `Pulsing<Black,${color},2000>`;
case "humpflicker": return `HumpFlicker<${color},Black,100>`;
case "stripes": return `Stripes<10,-700,${color},Black,Black>`;
case "cylon": return `Cylon<${color},6,20>`;
case "strobe": return `Strobe<Black,${color},15,1>`;
case "solid": return `${color}`;
case "runsegment": return `TransitionLoop<Black,TrConcat<TrWipe<1000>,${color},TrWipe<1000>>,${barLen}>`;
default: return `Black`;
}
}
let code = "";
if (currentMode === "16") {
const codes = [];
const sectionSize = 32768 / 16;
for (let i = 0; i < 16; i++) {
const isOuter = i % 2 === 0;
const effect = isOuter ? outerEffect : innerEffect;
const color = isOuter ? rgbText(outerColor) : rgbText(innerColor);
const barLen = isOuter ? outerBarLen : innerBarLen;
const eCode = effectCode(effect, color, barLen);
const center = Math.round((i + 0.5) * sectionSize);
const width = Math.round(sectionSize);
codes.push(`AlphaL<${eCode},LinearSectionF<Int<${center}>,Int<${width}>>>`);
}
code = `StylePtr<Layers<\n ${codes.join(",\n ")}\n>>(),`;
} else {
// 5-pixel: all outer only, divide space equally
const effect = outerEffect;
const color = rgbText(outerColor);
const barLen = outerBarLen;
const eCode = effectCode(effect, color, barLen);
const codes = [];
const sectionSize = 32768 / 5;
for (let i = 0; i < 5; i++) {
const center = Math.round((i + 0.5) * sectionSize);
const width = Math.round(sectionSize);
codes.push(`AlphaL<${eCode},LinearSectionF<Int<${center}>,Int<${width}>>>`);
}
code = `StylePtr<Layers<\n ${codes.join(",\n ")}\n>>(),`;
}
window._proffieGeneratedCode = code;
document.getElementById("copyCodeBtn").style.display = "";
document.getElementById("copyStatus").textContent = "";
// Optionally, hide the <pre> if you want:
// document.getElementById("exportOutput").style.display = "none";
}
updateSpeedDisplay();
draw();
setPixelMode();
</script>
</body>
</html>
brilliant man, its looking good i like all the improvements you’ve made, i was trying last night to add battery level styles but it kept messing up, i was trying to add a check box next to the effects drop down menus for each ring to convert selected effect to a battery level indicator
Do you have a link for this style editor Brian?
Still a WIP.
I’ll github/jekyl it when i get a few more things done soon.