New effects, refactor
This commit is contained in:
parent
4ac45367e5
commit
c0d6bee9c3
14 changed files with 1620 additions and 215 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { ButtonEffect } from '../effect-base.js';
|
||||
import { ButtonEffect } from "../effect-base.js";
|
||||
|
||||
/**
|
||||
* Border effect
|
||||
|
|
@ -7,65 +7,107 @@ import { ButtonEffect } from '../effect-base.js';
|
|||
export class BorderEffect extends ButtonEffect {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'border',
|
||||
name: 'Border',
|
||||
type: 'border',
|
||||
category: 'Border',
|
||||
renderOrder: 10
|
||||
id: "border",
|
||||
name: "Border",
|
||||
type: "border",
|
||||
category: "Border",
|
||||
renderOrder: 10,
|
||||
});
|
||||
}
|
||||
|
||||
defineControls() {
|
||||
return [
|
||||
{
|
||||
id: 'border-width',
|
||||
type: 'range',
|
||||
label: 'Border Width',
|
||||
id: "border-width",
|
||||
type: "range",
|
||||
label: "Border Width",
|
||||
defaultValue: 2,
|
||||
min: 0,
|
||||
max: 5,
|
||||
step: 1,
|
||||
description: 'Width of border in pixels'
|
||||
description: "Width of border in pixels",
|
||||
},
|
||||
{
|
||||
id: 'border-color',
|
||||
type: 'color',
|
||||
label: 'Border Color',
|
||||
defaultValue: '#000000'
|
||||
id: "border-color",
|
||||
type: "color",
|
||||
label: "Border Color",
|
||||
defaultValue: "#000000",
|
||||
},
|
||||
{
|
||||
id: 'border-style',
|
||||
type: 'select',
|
||||
label: 'Border Style',
|
||||
defaultValue: 'solid',
|
||||
id: "border-style",
|
||||
type: "select",
|
||||
label: "Border Style",
|
||||
defaultValue: "solid",
|
||||
options: [
|
||||
{ value: 'solid', label: 'Solid' },
|
||||
{ value: 'inset', label: 'Inset (3D)' },
|
||||
{ value: 'outset', label: 'Outset (3D)' },
|
||||
{ value: 'ridge', label: 'Ridge' }
|
||||
]
|
||||
}
|
||||
{ value: "solid", label: "Solid" },
|
||||
{ value: "dashed", label: "Dashed" },
|
||||
{ value: "dotted", label: "Dotted" },
|
||||
{ value: "double", label: "Double" },
|
||||
{ value: "inset", label: "Inset (3D)" },
|
||||
{ value: "outset", label: "Outset (3D)" },
|
||||
{ value: "ridge", label: "Ridge" },
|
||||
{ value: "rainbow", label: "Rainbow (Animated)" },
|
||||
{ value: "marching-ants", label: "Marching Ants" },
|
||||
{ value: "checkerboard", label: "Checkerboard" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "border-rainbow-speed",
|
||||
type: "range",
|
||||
label: "Rainbow Speed",
|
||||
defaultValue: 1,
|
||||
min: 1,
|
||||
max: 3,
|
||||
step: 1,
|
||||
showWhen: "border-style",
|
||||
description: "Speed of rainbow animation",
|
||||
},
|
||||
{
|
||||
id: "border-march-speed",
|
||||
type: "range",
|
||||
label: "March Speed",
|
||||
defaultValue: 1,
|
||||
min: 1,
|
||||
max: 3,
|
||||
step: 1,
|
||||
showWhen: "border-style",
|
||||
description: "Speed of marching animation",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
isEnabled(controlValues) {
|
||||
const width = controlValues['border-width'] || 0;
|
||||
const width = controlValues["border-width"] || 0;
|
||||
return width > 0;
|
||||
}
|
||||
|
||||
apply(context, controlValues, animState, renderData) {
|
||||
const width = controlValues['border-width'] || 0;
|
||||
const width = controlValues["border-width"] || 0;
|
||||
if (width === 0) return;
|
||||
|
||||
const color = controlValues['border-color'] || '#000000';
|
||||
const style = controlValues['border-style'] || 'solid';
|
||||
const color = controlValues["border-color"] || "#000000";
|
||||
const style = controlValues["border-style"] || "solid";
|
||||
|
||||
if (style === 'solid') {
|
||||
if (style === "solid") {
|
||||
this.drawSolidBorder(context, width, color, renderData);
|
||||
} else if (style === 'inset' || style === 'outset') {
|
||||
this.draw3DBorder(context, width, style === 'outset', renderData);
|
||||
} else if (style === 'ridge') {
|
||||
} else if (style === "dashed") {
|
||||
this.drawDashedBorder(context, width, color, renderData);
|
||||
} else if (style === "dotted") {
|
||||
this.drawDottedBorder(context, width, color, renderData);
|
||||
} else if (style === "double") {
|
||||
this.drawDoubleBorder(context, width, color, renderData);
|
||||
} else if (style === "inset" || style === "outset") {
|
||||
this.draw3DBorder(context, width, color, style === "outset", renderData);
|
||||
} else if (style === "ridge") {
|
||||
this.drawRidgeBorder(context, width, renderData);
|
||||
} else if (style === "rainbow") {
|
||||
const speed = controlValues["border-rainbow-speed"] || 1;
|
||||
this.drawRainbowBorder(context, width, animState, speed, renderData);
|
||||
} else if (style === "marching-ants") {
|
||||
const speed = controlValues["border-march-speed"] || 1;
|
||||
this.drawMarchingAntsBorder(context, width, animState, speed, renderData);
|
||||
} else if (style === "checkerboard") {
|
||||
this.drawCheckerboardBorder(context, width, color, renderData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,33 +121,65 @@ export class BorderEffect extends ButtonEffect {
|
|||
width / 2,
|
||||
width / 2,
|
||||
renderData.width - width,
|
||||
renderData.height - width
|
||||
renderData.height - width,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw 3D inset/outset border
|
||||
*/
|
||||
draw3DBorder(context, width, isOutset, renderData) {
|
||||
const lightColor = isOutset ? '#ffffff' : '#000000';
|
||||
const darkColor = isOutset ? '#000000' : '#ffffff';
|
||||
draw3DBorder(context, width, color, isOutset, renderData) {
|
||||
const w = renderData.width;
|
||||
const h = renderData.height;
|
||||
const t = width;
|
||||
|
||||
// Top and left (light)
|
||||
context.strokeStyle = lightColor;
|
||||
context.lineWidth = width;
|
||||
context.beginPath();
|
||||
context.moveTo(0, renderData.height);
|
||||
context.lineTo(0, 0);
|
||||
context.lineTo(renderData.width, 0);
|
||||
context.stroke();
|
||||
const normalized = color.toLowerCase();
|
||||
const isPureBlack = normalized === "#000000";
|
||||
const isPureWhite = normalized === "#ffffff";
|
||||
|
||||
// Bottom and right (dark)
|
||||
context.strokeStyle = darkColor;
|
||||
context.beginPath();
|
||||
context.moveTo(renderData.width, 0);
|
||||
context.lineTo(renderData.width, renderData.height);
|
||||
context.lineTo(0, renderData.height);
|
||||
context.stroke();
|
||||
let lightColor;
|
||||
let darkColor;
|
||||
|
||||
if (isPureBlack || isPureWhite) {
|
||||
lightColor = isOutset ? "#ffffff" : "#000000";
|
||||
darkColor = isOutset ? "#000000" : "#ffffff";
|
||||
} else {
|
||||
const lighter = this.adjustColor(color, 0.25);
|
||||
const darker = this.adjustColor(color, -0.25);
|
||||
|
||||
lightColor = isOutset ? lighter : darker;
|
||||
darkColor = isOutset ? darker : lighter;
|
||||
}
|
||||
|
||||
context.fillStyle = lightColor;
|
||||
context.fillRect(0, 0, w - t, t);
|
||||
context.fillRect(0, t, t, h - t);
|
||||
|
||||
context.fillStyle = darkColor;
|
||||
context.fillRect(t, h - t, w - t, t);
|
||||
context.fillRect(w - t, 0, t, h - t);
|
||||
|
||||
this.drawBevelCorners(context, t, w, h, lightColor, darkColor, isOutset);
|
||||
}
|
||||
|
||||
drawBevelCorners(ctx, t, w, h, light, dark, isOutset) {
|
||||
// Top-left corner
|
||||
ctx.fillStyle = dark;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, h);
|
||||
ctx.lineTo(t, h);
|
||||
ctx.lineTo(t, h - t);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Bottom-right corner
|
||||
ctx.fillStyle = light;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(w - t, 0);
|
||||
ctx.lineTo(w - t, t);
|
||||
ctx.lineTo(w, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -113,24 +187,212 @@ export class BorderEffect extends ButtonEffect {
|
|||
*/
|
||||
drawRidgeBorder(context, width, renderData) {
|
||||
// Outer ridge (light)
|
||||
context.strokeStyle = '#ffffff';
|
||||
context.strokeStyle = "#ffffff";
|
||||
context.lineWidth = width / 2;
|
||||
context.strokeRect(
|
||||
width / 4,
|
||||
width / 4,
|
||||
renderData.width - width / 2,
|
||||
renderData.height - width / 2
|
||||
renderData.height - width / 2,
|
||||
);
|
||||
|
||||
// Inner ridge (dark)
|
||||
context.strokeStyle = '#000000';
|
||||
context.strokeStyle = "#000000";
|
||||
context.strokeRect(
|
||||
(width * 3) / 4,
|
||||
(width * 3) / 4,
|
||||
renderData.width - width * 1.5,
|
||||
renderData.height - width * 1.5
|
||||
renderData.height - width * 1.5,
|
||||
);
|
||||
}
|
||||
|
||||
adjustColor(hex, amount) {
|
||||
// hex: "#rrggbb", amount: -1.0 .. 1.0
|
||||
let r = parseInt(hex.slice(1, 3), 16);
|
||||
let g = parseInt(hex.slice(3, 5), 16);
|
||||
let b = parseInt(hex.slice(5, 7), 16);
|
||||
|
||||
const adjust = (c) =>
|
||||
Math.min(255, Math.max(0, Math.round(c + amount * 255)));
|
||||
|
||||
r = adjust(r);
|
||||
g = adjust(g);
|
||||
b = adjust(b);
|
||||
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw dashed border
|
||||
*/
|
||||
drawDashedBorder(context, width, color, renderData) {
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = width;
|
||||
context.setLineDash([6, 3]); // 6px dash, 3px gap
|
||||
context.strokeRect(
|
||||
width / 2,
|
||||
width / 2,
|
||||
renderData.width - width,
|
||||
renderData.height - width
|
||||
);
|
||||
context.setLineDash([]); // Reset to solid
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw dotted border
|
||||
*/
|
||||
drawDottedBorder(context, width, color, renderData) {
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = width;
|
||||
context.setLineDash([2, 3]); // 2px dot, 3px gap
|
||||
context.lineCap = "round";
|
||||
context.strokeRect(
|
||||
width / 2,
|
||||
width / 2,
|
||||
renderData.width - width,
|
||||
renderData.height - width
|
||||
);
|
||||
context.setLineDash([]); // Reset to solid
|
||||
context.lineCap = "butt";
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw double border
|
||||
*/
|
||||
drawDoubleBorder(context, width, color, renderData) {
|
||||
const gap = Math.max(1, Math.floor(width / 3));
|
||||
const lineWidth = Math.max(1, Math.floor((width - gap) / 2));
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = lineWidth;
|
||||
|
||||
// Outer border
|
||||
context.strokeRect(
|
||||
lineWidth / 2,
|
||||
lineWidth / 2,
|
||||
renderData.width - lineWidth,
|
||||
renderData.height - lineWidth
|
||||
);
|
||||
|
||||
// Inner border
|
||||
const innerOffset = lineWidth + gap;
|
||||
context.strokeRect(
|
||||
innerOffset + lineWidth / 2,
|
||||
innerOffset + lineWidth / 2,
|
||||
renderData.width - innerOffset * 2 - lineWidth,
|
||||
renderData.height - innerOffset * 2 - lineWidth
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw rainbow animated border
|
||||
*/
|
||||
drawRainbowBorder(context, width, animState, speed, renderData) {
|
||||
if (!animState) {
|
||||
// Fallback to solid if no animation
|
||||
this.drawSolidBorder(context, width, "#ff0000", renderData);
|
||||
return;
|
||||
}
|
||||
|
||||
const hue = (animState.progress * speed * 360) % 360;
|
||||
const color = `hsl(${hue}, 80%, 50%)`;
|
||||
|
||||
context.strokeStyle = color;
|
||||
context.lineWidth = width;
|
||||
context.strokeRect(
|
||||
width / 2,
|
||||
width / 2,
|
||||
renderData.width - width,
|
||||
renderData.height - width
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw marching ants animated border
|
||||
*/
|
||||
drawMarchingAntsBorder(context, width, animState, speed, renderData) {
|
||||
if (!animState) {
|
||||
// Fallback to dashed if no animation
|
||||
this.drawDashedBorder(context, width, "#000000", renderData);
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate the dash offset using phase for smooth looping
|
||||
const phase = animState.getPhase(speed);
|
||||
const dashLength = 9; // 4px dash + 5px gap = 9px total
|
||||
const offset = (phase / (Math.PI * 2)) * dashLength;
|
||||
|
||||
context.strokeStyle = "#000000";
|
||||
context.lineWidth = width;
|
||||
context.setLineDash([4, 5]);
|
||||
context.lineDashOffset = -offset;
|
||||
context.strokeRect(
|
||||
width / 2,
|
||||
width / 2,
|
||||
renderData.width - width,
|
||||
renderData.height - width
|
||||
);
|
||||
context.setLineDash([]);
|
||||
context.lineDashOffset = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw checkerboard border
|
||||
*/
|
||||
drawCheckerboardBorder(context, width, color, renderData) {
|
||||
const squareSize = Math.max(2, width);
|
||||
const w = renderData.width;
|
||||
const h = renderData.height;
|
||||
|
||||
// Parse the color
|
||||
const r = parseInt(color.slice(1, 3), 16);
|
||||
const g = parseInt(color.slice(3, 5), 16);
|
||||
const b = parseInt(color.slice(5, 7), 16);
|
||||
|
||||
// Create light and dark versions
|
||||
const darkColor = color;
|
||||
const lightColor = `rgb(${Math.min(255, r + 60)}, ${Math.min(
|
||||
255,
|
||||
g + 60
|
||||
)}, ${Math.min(255, b + 60)})`;
|
||||
|
||||
// Draw checkerboard on all four sides
|
||||
// Top
|
||||
for (let x = 0; x < w; x += squareSize) {
|
||||
for (let y = 0; y < width; y += squareSize) {
|
||||
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||
context.fillRect(x, y, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom
|
||||
for (let x = 0; x < w; x += squareSize) {
|
||||
for (let y = h - width; y < h; y += squareSize) {
|
||||
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||
context.fillRect(x, y, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Left
|
||||
for (let x = 0; x < width; x += squareSize) {
|
||||
for (let y = width; y < h - width; y += squareSize) {
|
||||
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||
context.fillRect(x, y, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Right
|
||||
for (let x = w - width; x < w; x += squareSize) {
|
||||
for (let y = width; y < h - width; y += squareSize) {
|
||||
const checker = Math.floor(x / squareSize + y / squareSize) % 2;
|
||||
context.fillStyle = checker === 0 ? darkColor : lightColor;
|
||||
context.fillRect(x, y, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-register effect
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue