mirror of
https://github.com/SamEyeBam/animate.git
synced 2026-02-04 01:14:15 +00:00
V1.1
Giant refactor. added layers. ui overhaul. added save/load and we now got presets
This commit is contained in:
2
.vscode/settings.json
vendored
Normal file
2
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -23,13 +23,13 @@ canvas {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 0px 20px 0px 20px;
|
padding: 0px 20px 0px 20px;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: rgba(32, 32, 32, 0.616);
|
background-color: rgba(32, 32, 32, 0.616);
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
#toolbar {
|
#toolbar {
|
||||||
@@ -38,43 +38,11 @@ canvas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#shape-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
/* position: absolute; */
|
|
||||||
padding: 0px 20px 0px 20px;
|
|
||||||
/* width: 500px; */
|
|
||||||
/* height: 100vh; */
|
|
||||||
/* background-color: rgba(32, 32, 32, 0.616); */
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#shape-controls p{
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: Roboto, system-ui;
|
|
||||||
}
|
|
||||||
#toolbar p{
|
#toolbar p{
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
font-family: Roboto, system-ui;
|
font-family: Roboto, system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0px 0px 0px 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 0px 0px 0px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -149,4 +117,820 @@ canvas {
|
|||||||
|
|
||||||
.buttonReset{
|
.buttonReset{
|
||||||
background-color: #f4e1e1;
|
background-color: #f4e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
Control System Styles
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
.control-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(40, 45, 50, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-range-container {
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-main-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label-inline {
|
||||||
|
color: #b0b8c0;
|
||||||
|
font-family: Roboto, system-ui;
|
||||||
|
font-size: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-value {
|
||||||
|
color: #4a9eff;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-label {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: Roboto, system-ui;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-range {
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 6px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: #3a4a5a;
|
||||||
|
border-radius: 3px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-range::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-range::-moz-range-thumb {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-color {
|
||||||
|
width: 40px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-checkbox {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
accent-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-dropdown {
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #555;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Options row with toggle buttons */
|
||||||
|
.control-options-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-toggle-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a4a5a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6a7a8a;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-toggle-btn:hover {
|
||||||
|
background: #3a4a5a;
|
||||||
|
color: #a0b0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-toggle-btn.active {
|
||||||
|
background: #4a9eff20;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-toggle-btn.has-filters {
|
||||||
|
background: #4a9eff30;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings panel */
|
||||||
|
.control-settings-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
background: rgba(30, 35, 40, 0.6);
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 2px solid #5a6a7a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
color: #8a9aa0;
|
||||||
|
font-size: 11px;
|
||||||
|
min-width: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-input {
|
||||||
|
background: #2a3a4a;
|
||||||
|
border: 1px solid #4a5a6a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filters panel */
|
||||||
|
.control-filters-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-filter-row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-select {
|
||||||
|
width: 100%;
|
||||||
|
background: #2a3a4a;
|
||||||
|
border: 1px dashed #4a5a6a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #8a9aa0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-select:hover {
|
||||||
|
border-color: #6a7a8a;
|
||||||
|
color: #a0b0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual filter item */
|
||||||
|
.filter-item {
|
||||||
|
background: rgba(40, 50, 60, 0.5);
|
||||||
|
border-left: 3px solid #4a9eff;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-item-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-type-dropdown {
|
||||||
|
flex: 1;
|
||||||
|
background: #2a3a4a;
|
||||||
|
border: 1px solid #4a5a6a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #8ac4ff;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-settings-toggle {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #3a4a5a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #6a7a8a;
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-settings-toggle:hover,
|
||||||
|
.filter-settings-toggle.active {
|
||||||
|
background: #3a4a5a;
|
||||||
|
color: #a0b0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-remove-btn {
|
||||||
|
background: #5a3a3a;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ff8a8a;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-remove-btn:hover {
|
||||||
|
background: #7a4a4a;
|
||||||
|
color: #ffaaaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter settings panel */
|
||||||
|
.filter-settings-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-row {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-top-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-label {
|
||||||
|
color: #8a9aa0;
|
||||||
|
font-size: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-value {
|
||||||
|
color: #6a9aff;
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
min-width: 35px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-settings-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #5a6a7a;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-settings-btn:hover {
|
||||||
|
color: #8a9aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-slider {
|
||||||
|
width: 100%;
|
||||||
|
height: 4px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: #3a4a5a;
|
||||||
|
border-radius: 2px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-slider::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #6a9aff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-param-slider::-moz-range-thumb {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #6a9aff;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Parameter bounds panel */
|
||||||
|
.param-bounds-panel {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
background: rgba(30, 40, 50, 0.5);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-bounds-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-bound-input {
|
||||||
|
flex: 1;
|
||||||
|
background: #2a3a4a;
|
||||||
|
border: 1px solid #4a5a6a;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #a0b0c0;
|
||||||
|
padding: 3px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Scene / Multi-Shape UI ===== */
|
||||||
|
|
||||||
|
.scene-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-btn {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #3a3a3a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-btn:hover {
|
||||||
|
background: #4a4a4a;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-shape-btn {
|
||||||
|
background: #4a9eff;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#add-shape-btn:hover {
|
||||||
|
background: #3a8eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legacy - keep for compatibility */
|
||||||
|
.add-shape-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-shape-btn:hover {
|
||||||
|
background: #3a8eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Presets modal */
|
||||||
|
.presets-modal {
|
||||||
|
position: fixed;
|
||||||
|
padding: 0px 20px;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-content {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-content h3 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-actions {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-save-current-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-save-current-btn:hover {
|
||||||
|
background: #3a8eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 100px;
|
||||||
|
max-height: 300px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-empty {
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
background: #3a3a3a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-name {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-layers {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-load-btn {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #4a9eff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-load-btn:hover {
|
||||||
|
background: #3a8eef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-delete-btn {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
background: #444;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-delete-btn:hover {
|
||||||
|
background: #c44;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-cancel {
|
||||||
|
width: 100%;
|
||||||
|
background: #555;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-cancel:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-section-header {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding: 12px 0 6px 0;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presets-section-header:first-child {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-builtin {
|
||||||
|
border-color: #4a9eff33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-description {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.presets-modal {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layer panels */
|
||||||
|
.layer-panel {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #252525;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-collapse-btn {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #888;
|
||||||
|
font-size: 10px;
|
||||||
|
width: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-collapse-btn:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-name {
|
||||||
|
flex: 1;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ccc;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-btn:hover {
|
||||||
|
background: #444;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-remove:hover {
|
||||||
|
background: #c44;
|
||||||
|
border-color: #c44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layer-content {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layers container */
|
||||||
|
#layers-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shape picker modal */
|
||||||
|
.shape-picker-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 500px;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-container {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
max-height: 80vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-container h3 {
|
||||||
|
color: #e0e0e0;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-tab {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #333;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-tab:hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-tab.active {
|
||||||
|
background: #4a9eff;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-content {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-section-title {
|
||||||
|
color: #888;
|
||||||
|
font-size: 11px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-buttons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-btn {
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
text-align: left;
|
||||||
|
background: #3a3a3a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-btn:hover {
|
||||||
|
background: #4a4a4a;
|
||||||
|
border-color: #4a9eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-btn.preset-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-btn-info {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-cancel {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
background: #555;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shape-picker-cancel:hover {
|
||||||
|
background: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive for shape picker */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.shape-picker-modal {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,24 +10,8 @@
|
|||||||
<body style="margin:0;">
|
<body style="margin:0;">
|
||||||
<canvas id="myCanvas" style="display: block;box-sizing: border-box;"></canvas>
|
<canvas id="myCanvas" style="display: block;box-sizing: border-box;"></canvas>
|
||||||
<div id="toolbar">
|
<div id="toolbar">
|
||||||
<br>
|
<!-- Shape layers -->
|
||||||
<select id="shape-selector">
|
<div id="layers-container"></div>
|
||||||
<option value="RaysInShape">Rays</option>
|
|
||||||
<option value="Countdown">Countdown</option>
|
|
||||||
<option value="NewWave">NewWave</option>
|
|
||||||
<option value="EyePrototype">EyePrototype</option>
|
|
||||||
<option value="Nodal_expanding">Nodal_expanding</option>
|
|
||||||
<option value="MaryFace">MaryFace</option>
|
|
||||||
<option value="CircleExpand">CircleExpand</option>
|
|
||||||
<option value="PolyTwistColourWidth">PolyTwistColourWidth</option>
|
|
||||||
<option value="FloralPhyllo">FloralPhyllo</option>
|
|
||||||
<option value="FloralPhyllo_Accident">FloralPhyllo_Accident</option>
|
|
||||||
<option value="Spiral1">Spiral1</option>
|
|
||||||
<option value="FloralAccident">FloralAccident</option>
|
|
||||||
<option value="Phyllotaxis">Phyllotaxis</option>
|
|
||||||
<option value="SquareTwist_angle">SquareTwist_angle</option>
|
|
||||||
</select>
|
|
||||||
<div id="shape-controls"></div>
|
|
||||||
<br>
|
<br>
|
||||||
<p>Controls:</p>
|
<p>Controls:</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -52,10 +36,40 @@
|
|||||||
<button onclick="manualToggleSettings()" class="button">Show/hide</button>
|
<button onclick="manualToggleSettings()" class="button">Show/hide</button>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
<script src="./js/helper.js" type="text/javascript"></script>
|
<!-- Utilities (must load first - used by everything) -->
|
||||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
<script src="./js/utils/helpers.js" type="text/javascript"></script>
|
||||||
<script src="./js/math.js" type="text/javascript"></script>
|
<script src="./js/math.js" type="text/javascript"></script>
|
||||||
<script src="./js/objects.js" type="text/javascript"></script>
|
|
||||||
<script src="./js/index.js" ></script>
|
<!-- Core modules -->
|
||||||
|
<script src="./js/core/ShapeRegistry.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/core/AnimationEngine.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/core/Scene.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/core/BaseShape.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- Control system -->
|
||||||
|
<script src="./js/controls/FilterManager.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/controls/ControlFactory.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/controls/ControlManager.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/presets.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/controls/SceneUI.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- Shape definitions -->
|
||||||
|
<script src="./js/shapes/PolyTwistColourWidth.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/FloralPhyllo.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/Spiral1.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/FloralAccident.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/FloralPhyllo_Accident.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/Nodal_expanding.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/Phyllotaxis.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/SquareTwist_angle.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/CircleExpand.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/EyePrototype.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/MaryFace.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/NewWave.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/Countdown.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/shapes/RaysInShape.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- Main application -->
|
||||||
|
<script src="./js/index.js" type="text/javascript"></script>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
667
docs/js/controls/ControlFactory.js
Normal file
667
docs/js/controls/ControlFactory.js
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
/**
|
||||||
|
* ControlFactory - Creates DOM elements for different control types
|
||||||
|
*
|
||||||
|
* Separated from ControlManager for cleaner code organization
|
||||||
|
* Features collapsible settings panels and inline filter configuration
|
||||||
|
*/
|
||||||
|
class ControlFactory {
|
||||||
|
constructor(controlManager) {
|
||||||
|
this.controlManager = controlManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a control based on type
|
||||||
|
* @param {object} config - Control configuration
|
||||||
|
* @param {object} instance - Shape instance to bind to
|
||||||
|
* @param {HTMLElement} container - Container element
|
||||||
|
* @returns {object} Control data { element, listener, type, config }
|
||||||
|
*/
|
||||||
|
create(config, instance, container) {
|
||||||
|
switch (config.type) {
|
||||||
|
case 'range':
|
||||||
|
return this.createRange(config, instance, container);
|
||||||
|
case 'color':
|
||||||
|
return this.createColor(config, instance, container);
|
||||||
|
case 'checkbox':
|
||||||
|
return this.createCheckbox(config, instance, container);
|
||||||
|
case 'dropdown':
|
||||||
|
return this.createDropdown(config, instance, container);
|
||||||
|
case 'button':
|
||||||
|
return this.createButton(config, instance, container);
|
||||||
|
case 'header':
|
||||||
|
return this.createHeader(config, container);
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown control type: ${config.type}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a collapsible section with a toggle arrow
|
||||||
|
*/
|
||||||
|
createCollapsible(title, className, defaultOpen = false) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = `collapsible-section ${className}`;
|
||||||
|
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'collapsible-header';
|
||||||
|
|
||||||
|
const arrow = document.createElement('span');
|
||||||
|
arrow.className = 'collapsible-arrow';
|
||||||
|
arrow.textContent = defaultOpen ? '▼' : '▶';
|
||||||
|
|
||||||
|
const titleSpan = document.createElement('span');
|
||||||
|
titleSpan.className = 'collapsible-title';
|
||||||
|
titleSpan.textContent = title;
|
||||||
|
|
||||||
|
header.appendChild(arrow);
|
||||||
|
header.appendChild(titleSpan);
|
||||||
|
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'collapsible-content';
|
||||||
|
content.style.display = defaultOpen ? 'block' : 'none';
|
||||||
|
|
||||||
|
header.addEventListener('click', () => {
|
||||||
|
const isOpen = content.style.display !== 'none';
|
||||||
|
content.style.display = isOpen ? 'none' : 'block';
|
||||||
|
arrow.textContent = isOpen ? '▶' : '▼';
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(header);
|
||||||
|
wrapper.appendChild(content);
|
||||||
|
|
||||||
|
return { wrapper, content, header, arrow };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a range slider control with collapsible settings and filters
|
||||||
|
*/
|
||||||
|
createRange(config, instance, container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'control-container control-range-container';
|
||||||
|
|
||||||
|
// Main row: label + value + slider
|
||||||
|
const mainRow = document.createElement('div');
|
||||||
|
mainRow.className = 'control-main-row';
|
||||||
|
|
||||||
|
// Label showing property name
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'control-label-inline';
|
||||||
|
label.textContent = config.property;
|
||||||
|
|
||||||
|
// Value display (editable)
|
||||||
|
const valueDisplay = document.createElement('span');
|
||||||
|
valueDisplay.className = 'control-value';
|
||||||
|
valueDisplay.id = `elText${config.property}`;
|
||||||
|
valueDisplay.textContent = instance[config.property] ?? config.defaultValue;
|
||||||
|
|
||||||
|
// Range input
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'range';
|
||||||
|
input.className = 'control control-range';
|
||||||
|
input.id = `el${config.property}`;
|
||||||
|
input.min = config.min;
|
||||||
|
input.max = config.max;
|
||||||
|
input.value = instance[config.property] ?? config.defaultValue;
|
||||||
|
|
||||||
|
// Store original config bounds for reference
|
||||||
|
const configState = {
|
||||||
|
min: config.min,
|
||||||
|
max: config.max,
|
||||||
|
originalMin: config.min,
|
||||||
|
originalMax: config.max
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listener
|
||||||
|
const listener = (event) => {
|
||||||
|
const newValue = parseFloat(event.target.value);
|
||||||
|
instance[config.property] = newValue;
|
||||||
|
valueDisplay.textContent = Math.round(newValue * 100) / 100;
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(instance, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('input', listener);
|
||||||
|
|
||||||
|
mainRow.appendChild(label);
|
||||||
|
mainRow.appendChild(valueDisplay);
|
||||||
|
wrapper.appendChild(mainRow);
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
|
||||||
|
// Options row with toggle buttons
|
||||||
|
const optionsRow = document.createElement('div');
|
||||||
|
optionsRow.className = 'control-options-row';
|
||||||
|
|
||||||
|
// Settings toggle button
|
||||||
|
const settingsBtn = document.createElement('button');
|
||||||
|
settingsBtn.className = 'control-toggle-btn';
|
||||||
|
settingsBtn.innerHTML = '⚙';
|
||||||
|
settingsBtn.title = 'Settings';
|
||||||
|
|
||||||
|
// Filters toggle button
|
||||||
|
const filtersBtn = document.createElement('button');
|
||||||
|
filtersBtn.className = 'control-toggle-btn';
|
||||||
|
filtersBtn.innerHTML = '◇';
|
||||||
|
filtersBtn.title = 'Filters';
|
||||||
|
|
||||||
|
optionsRow.appendChild(settingsBtn);
|
||||||
|
optionsRow.appendChild(filtersBtn);
|
||||||
|
wrapper.appendChild(optionsRow);
|
||||||
|
|
||||||
|
// Settings panel (hidden by default)
|
||||||
|
const settingsPanel = document.createElement('div');
|
||||||
|
settingsPanel.className = 'control-settings-panel';
|
||||||
|
settingsPanel.style.display = 'none';
|
||||||
|
|
||||||
|
// Min setting
|
||||||
|
const minRow = this.createSettingRow('Min', configState.min, (val) => {
|
||||||
|
configState.min = val;
|
||||||
|
input.min = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Max setting
|
||||||
|
const maxRow = this.createSettingRow('Max', configState.max, (val) => {
|
||||||
|
configState.max = val;
|
||||||
|
input.max = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
settingsPanel.appendChild(minRow);
|
||||||
|
settingsPanel.appendChild(maxRow);
|
||||||
|
wrapper.appendChild(settingsPanel);
|
||||||
|
|
||||||
|
// Settings toggle
|
||||||
|
settingsBtn.addEventListener('click', () => {
|
||||||
|
const isOpen = settingsPanel.style.display !== 'none';
|
||||||
|
settingsPanel.style.display = isOpen ? 'none' : 'block';
|
||||||
|
settingsBtn.classList.toggle('active', !isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filters panel (hidden by default)
|
||||||
|
const filtersPanel = document.createElement('div');
|
||||||
|
filtersPanel.className = 'control-filters-panel';
|
||||||
|
filtersPanel.style.display = 'none';
|
||||||
|
|
||||||
|
// Add filter dropdown
|
||||||
|
const addFilterRow = document.createElement('div');
|
||||||
|
addFilterRow.className = 'add-filter-row';
|
||||||
|
|
||||||
|
const filterSelect = document.createElement('select');
|
||||||
|
filterSelect.className = 'filter-type-select';
|
||||||
|
filterSelect.innerHTML = '<option value="">+ Add Filter...</option>';
|
||||||
|
|
||||||
|
const filters = this.controlManager.filterManager.getAvailableFilters();
|
||||||
|
for (const filter of filters) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = filter.id;
|
||||||
|
opt.textContent = filter.name;
|
||||||
|
filterSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
addFilterRow.appendChild(filterSelect);
|
||||||
|
filtersPanel.appendChild(addFilterRow);
|
||||||
|
|
||||||
|
// Container for active filters
|
||||||
|
const filtersContainer = document.createElement('div');
|
||||||
|
filtersContainer.className = 'filters-list';
|
||||||
|
filtersPanel.appendChild(filtersContainer);
|
||||||
|
|
||||||
|
wrapper.appendChild(filtersPanel);
|
||||||
|
|
||||||
|
// Filters toggle
|
||||||
|
filtersBtn.addEventListener('click', () => {
|
||||||
|
const isOpen = filtersPanel.style.display !== 'none';
|
||||||
|
filtersPanel.style.display = isOpen ? 'none' : 'block';
|
||||||
|
filtersBtn.classList.toggle('active', !isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
const controlData = {
|
||||||
|
element: input,
|
||||||
|
listener,
|
||||||
|
type: 'range',
|
||||||
|
config,
|
||||||
|
configState,
|
||||||
|
wrapper,
|
||||||
|
filtersContainer,
|
||||||
|
filters: [],
|
||||||
|
label,
|
||||||
|
valueDisplay,
|
||||||
|
settingsPanel,
|
||||||
|
filtersPanel,
|
||||||
|
filtersBtn
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter dropdown handler
|
||||||
|
filterSelect.addEventListener('change', () => {
|
||||||
|
if (filterSelect.value) {
|
||||||
|
this.controlManager.addFilter(controlData, config, filterSelect.value);
|
||||||
|
filterSelect.value = ''; // Reset dropdown
|
||||||
|
// Update filter button to show filter count
|
||||||
|
this.updateFilterBadge(controlData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return controlData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a setting row with label and number input
|
||||||
|
*/
|
||||||
|
createSettingRow(label, value, onChange) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'setting-row';
|
||||||
|
|
||||||
|
const labelEl = document.createElement('span');
|
||||||
|
labelEl.className = 'setting-label';
|
||||||
|
labelEl.textContent = label;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'number';
|
||||||
|
input.className = 'setting-input';
|
||||||
|
input.value = value;
|
||||||
|
|
||||||
|
input.addEventListener('change', (e) => {
|
||||||
|
onChange(parseFloat(e.target.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(labelEl);
|
||||||
|
row.appendChild(input);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update filter badge on button
|
||||||
|
*/
|
||||||
|
updateFilterBadge(controlData) {
|
||||||
|
const count = controlData.filters.length;
|
||||||
|
if (count > 0) {
|
||||||
|
controlData.filtersBtn.innerHTML = `◆ ${count}`;
|
||||||
|
controlData.filtersBtn.classList.add('has-filters');
|
||||||
|
} else {
|
||||||
|
controlData.filtersBtn.innerHTML = '◇';
|
||||||
|
controlData.filtersBtn.classList.remove('has-filters');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a color picker control
|
||||||
|
*/
|
||||||
|
createColor(config, instance, container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'control-container control-color-container';
|
||||||
|
|
||||||
|
const mainRow = document.createElement('div');
|
||||||
|
mainRow.className = 'control-main-row';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'control-label-inline';
|
||||||
|
label.textContent = config.property;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'color';
|
||||||
|
input.className = 'control control-color';
|
||||||
|
input.id = `el${config.property}`;
|
||||||
|
input.value = instance[config.property] ?? config.defaultValue;
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
instance[config.property] = newValue;
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(instance, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('input', listener);
|
||||||
|
|
||||||
|
mainRow.appendChild(label);
|
||||||
|
mainRow.appendChild(input);
|
||||||
|
wrapper.appendChild(mainRow);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: input,
|
||||||
|
listener,
|
||||||
|
type: 'color',
|
||||||
|
config,
|
||||||
|
wrapper,
|
||||||
|
label
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a checkbox control
|
||||||
|
*/
|
||||||
|
createCheckbox(config, instance, container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'control-container control-checkbox-container';
|
||||||
|
|
||||||
|
const mainRow = document.createElement('div');
|
||||||
|
mainRow.className = 'control-main-row';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'control-label-inline';
|
||||||
|
label.textContent = config.property;
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'checkbox';
|
||||||
|
input.className = 'control control-checkbox';
|
||||||
|
input.id = `el${config.property}`;
|
||||||
|
input.checked = instance[config.property] ?? config.defaultValue;
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const newValue = event.target.checked;
|
||||||
|
instance[config.property] = newValue;
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(instance, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.addEventListener('change', listener);
|
||||||
|
|
||||||
|
mainRow.appendChild(label);
|
||||||
|
mainRow.appendChild(input);
|
||||||
|
wrapper.appendChild(mainRow);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: input,
|
||||||
|
listener,
|
||||||
|
type: 'checkbox',
|
||||||
|
config,
|
||||||
|
wrapper,
|
||||||
|
label
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a dropdown select control
|
||||||
|
*/
|
||||||
|
createDropdown(config, instance, container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'control-container';
|
||||||
|
|
||||||
|
const label = document.createElement('p');
|
||||||
|
label.className = 'control-label';
|
||||||
|
label.id = `elText${config.property}`;
|
||||||
|
label.textContent = `${config.property}: ${config.defaultValue}`;
|
||||||
|
|
||||||
|
const select = document.createElement('select');
|
||||||
|
select.className = 'control control-dropdown';
|
||||||
|
select.id = `el${config.property}`;
|
||||||
|
|
||||||
|
for (const option of config.options || []) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = option.value;
|
||||||
|
opt.textContent = option.label;
|
||||||
|
select.appendChild(opt);
|
||||||
|
}
|
||||||
|
select.value = instance[config.property] ?? config.defaultValue;
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const newValue = event.target.value;
|
||||||
|
instance[config.property] = newValue;
|
||||||
|
label.textContent = `${config.property}: ${newValue}`;
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(instance, newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
select.addEventListener('change', listener);
|
||||||
|
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
wrapper.appendChild(select);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: select,
|
||||||
|
listener,
|
||||||
|
type: 'dropdown',
|
||||||
|
config,
|
||||||
|
wrapper,
|
||||||
|
label
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a button control
|
||||||
|
*/
|
||||||
|
createButton(config, instance, container) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'control-container';
|
||||||
|
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = 'control control-button button-8';
|
||||||
|
button.textContent = config.label || config.property;
|
||||||
|
|
||||||
|
const listener = () => {
|
||||||
|
if (config.method && typeof instance[config.method] === 'function') {
|
||||||
|
instance[config.method]();
|
||||||
|
}
|
||||||
|
if (config.callback) {
|
||||||
|
config.callback(instance);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
button.addEventListener('click', listener);
|
||||||
|
|
||||||
|
wrapper.appendChild(button);
|
||||||
|
container.appendChild(wrapper);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: button,
|
||||||
|
listener,
|
||||||
|
type: 'button',
|
||||||
|
config,
|
||||||
|
wrapper
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a header/separator
|
||||||
|
*/
|
||||||
|
createHeader(config, container) {
|
||||||
|
const header = document.createElement('p');
|
||||||
|
header.className = 'header';
|
||||||
|
header.id = `elHeader${(config.text || '').replace(/\s+/g, '')}`;
|
||||||
|
header.textContent = config.text || '';
|
||||||
|
container.appendChild(header);
|
||||||
|
|
||||||
|
return {
|
||||||
|
element: header,
|
||||||
|
listener: null,
|
||||||
|
type: 'header',
|
||||||
|
config,
|
||||||
|
wrapper: header
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create filter controls UI with collapsible settings
|
||||||
|
*/
|
||||||
|
createFilterUI(filterInstance, filterDef, controlData, controlConfig) {
|
||||||
|
const filterDiv = document.createElement('div');
|
||||||
|
filterDiv.className = 'filter-item';
|
||||||
|
|
||||||
|
// Header row with type dropdown and remove button
|
||||||
|
const headerRow = document.createElement('div');
|
||||||
|
headerRow.className = 'filter-item-header';
|
||||||
|
|
||||||
|
// Filter type dropdown (allows changing filter type)
|
||||||
|
const typeSelect = document.createElement('select');
|
||||||
|
typeSelect.className = 'filter-type-dropdown';
|
||||||
|
|
||||||
|
const filters = this.controlManager.filterManager.getAvailableFilters();
|
||||||
|
for (const filter of filters) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = filter.id;
|
||||||
|
opt.textContent = filter.name;
|
||||||
|
if (filter.id === filterInstance.type) {
|
||||||
|
opt.selected = true;
|
||||||
|
}
|
||||||
|
typeSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings toggle button
|
||||||
|
const settingsToggle = document.createElement('button');
|
||||||
|
settingsToggle.className = 'filter-settings-toggle';
|
||||||
|
settingsToggle.innerHTML = '⚙';
|
||||||
|
settingsToggle.title = 'Filter settings';
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
const removeBtn = document.createElement('button');
|
||||||
|
removeBtn.className = 'filter-remove-btn';
|
||||||
|
removeBtn.textContent = '×';
|
||||||
|
removeBtn.title = 'Remove filter';
|
||||||
|
|
||||||
|
headerRow.appendChild(typeSelect);
|
||||||
|
headerRow.appendChild(settingsToggle);
|
||||||
|
headerRow.appendChild(removeBtn);
|
||||||
|
filterDiv.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Settings panel (collapsible)
|
||||||
|
const settingsPanel = document.createElement('div');
|
||||||
|
settingsPanel.className = 'filter-settings-panel';
|
||||||
|
settingsPanel.style.display = 'none';
|
||||||
|
|
||||||
|
// Create controls for filter parameters
|
||||||
|
const paramInputs = {};
|
||||||
|
this.buildFilterParams(settingsPanel, filterInstance, filterDef, controlConfig, paramInputs);
|
||||||
|
|
||||||
|
filterDiv.appendChild(settingsPanel);
|
||||||
|
|
||||||
|
// Toggle settings
|
||||||
|
settingsToggle.addEventListener('click', () => {
|
||||||
|
const isOpen = settingsPanel.style.display !== 'none';
|
||||||
|
settingsPanel.style.display = isOpen ? 'none' : 'block';
|
||||||
|
settingsToggle.classList.toggle('active', !isOpen);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle filter type change
|
||||||
|
typeSelect.addEventListener('change', () => {
|
||||||
|
const newType = typeSelect.value;
|
||||||
|
const newFilterDef = this.controlManager.filterManager.getFilter(newType);
|
||||||
|
|
||||||
|
// Update filter instance type
|
||||||
|
filterInstance.type = newType;
|
||||||
|
|
||||||
|
// Reset params to defaults for new type
|
||||||
|
filterInstance.params = {};
|
||||||
|
for (const ctrl of newFilterDef.controls) {
|
||||||
|
if (ctrl.name === 'min') {
|
||||||
|
filterInstance.params.min = controlConfig.min ?? 0;
|
||||||
|
} else if (ctrl.name === 'max') {
|
||||||
|
filterInstance.params.max = controlConfig.max ?? 100;
|
||||||
|
} else {
|
||||||
|
filterInstance.params[ctrl.name] = ctrl.defaultValue ?? 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild params UI
|
||||||
|
settingsPanel.innerHTML = '';
|
||||||
|
this.buildFilterParams(settingsPanel, filterInstance, newFilterDef, controlConfig, paramInputs);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove handler
|
||||||
|
removeBtn.addEventListener('click', () => {
|
||||||
|
this.controlManager.removeFilter(controlData, filterInstance);
|
||||||
|
this.updateFilterBadge(controlData);
|
||||||
|
});
|
||||||
|
|
||||||
|
filterInstance.element = filterDiv;
|
||||||
|
return filterDiv;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build filter parameter controls
|
||||||
|
*/
|
||||||
|
buildFilterParams(container, filterInstance, filterDef, controlConfig, paramInputs) {
|
||||||
|
for (const paramDef of filterDef.controls) {
|
||||||
|
const paramRow = document.createElement('div');
|
||||||
|
paramRow.className = 'filter-param-row';
|
||||||
|
|
||||||
|
const paramLabel = document.createElement('span');
|
||||||
|
paramLabel.className = 'filter-param-label';
|
||||||
|
paramLabel.textContent = paramDef.label;
|
||||||
|
|
||||||
|
const paramValue = document.createElement('span');
|
||||||
|
paramValue.className = 'filter-param-value';
|
||||||
|
paramValue.textContent = filterInstance.params[paramDef.name];
|
||||||
|
|
||||||
|
const paramInput = document.createElement('input');
|
||||||
|
paramInput.type = 'range';
|
||||||
|
paramInput.className = 'filter-param-slider';
|
||||||
|
|
||||||
|
// Determine bounds
|
||||||
|
if (paramDef.name === 'min' || paramDef.name === 'max') {
|
||||||
|
// Use much wider bounds for filter min/max - can go beyond control range
|
||||||
|
paramInput.min = (controlConfig.min ?? 0) - Math.abs(controlConfig.max - controlConfig.min);
|
||||||
|
paramInput.max = (controlConfig.max ?? 100) + Math.abs(controlConfig.max - controlConfig.min);
|
||||||
|
paramInput.value = filterInstance.params[paramDef.name];
|
||||||
|
} else {
|
||||||
|
paramInput.min = paramDef.min ?? 0.1;
|
||||||
|
paramInput.max = paramDef.max ?? 10;
|
||||||
|
paramInput.step = paramDef.step ?? 0.1;
|
||||||
|
paramInput.value = filterInstance.params[paramDef.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings button for this parameter (to adjust its min/max)
|
||||||
|
const paramSettingsBtn = document.createElement('button');
|
||||||
|
paramSettingsBtn.className = 'param-settings-btn';
|
||||||
|
paramSettingsBtn.innerHTML = '⋯';
|
||||||
|
paramSettingsBtn.title = 'Adjust range';
|
||||||
|
|
||||||
|
// Mini settings panel for param bounds
|
||||||
|
const paramBoundsPanel = document.createElement('div');
|
||||||
|
paramBoundsPanel.className = 'param-bounds-panel';
|
||||||
|
paramBoundsPanel.style.display = 'none';
|
||||||
|
|
||||||
|
const boundsRow = document.createElement('div');
|
||||||
|
boundsRow.className = 'param-bounds-row';
|
||||||
|
|
||||||
|
const minInput = document.createElement('input');
|
||||||
|
minInput.type = 'number';
|
||||||
|
minInput.className = 'param-bound-input';
|
||||||
|
minInput.value = paramInput.min;
|
||||||
|
minInput.placeholder = 'min';
|
||||||
|
|
||||||
|
const maxInput = document.createElement('input');
|
||||||
|
maxInput.type = 'number';
|
||||||
|
maxInput.className = 'param-bound-input';
|
||||||
|
maxInput.value = paramInput.max;
|
||||||
|
maxInput.placeholder = 'max';
|
||||||
|
|
||||||
|
boundsRow.appendChild(minInput);
|
||||||
|
boundsRow.appendChild(maxInput);
|
||||||
|
paramBoundsPanel.appendChild(boundsRow);
|
||||||
|
|
||||||
|
minInput.addEventListener('change', () => {
|
||||||
|
paramInput.min = parseFloat(minInput.value);
|
||||||
|
});
|
||||||
|
maxInput.addEventListener('change', () => {
|
||||||
|
paramInput.max = parseFloat(maxInput.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
paramSettingsBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isOpen = paramBoundsPanel.style.display !== 'none';
|
||||||
|
paramBoundsPanel.style.display = isOpen ? 'none' : 'flex';
|
||||||
|
});
|
||||||
|
|
||||||
|
paramInput.addEventListener('input', (e) => {
|
||||||
|
const value = parseFloat(e.target.value);
|
||||||
|
filterInstance.params[paramDef.name] = value;
|
||||||
|
paramValue.textContent = Math.round(value * 100) / 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
const topRow = document.createElement('div');
|
||||||
|
topRow.className = 'filter-param-top-row';
|
||||||
|
topRow.appendChild(paramLabel);
|
||||||
|
topRow.appendChild(paramValue);
|
||||||
|
topRow.appendChild(paramSettingsBtn);
|
||||||
|
|
||||||
|
paramRow.appendChild(topRow);
|
||||||
|
paramRow.appendChild(paramInput);
|
||||||
|
paramRow.appendChild(paramBoundsPanel);
|
||||||
|
container.appendChild(paramRow);
|
||||||
|
|
||||||
|
paramInputs[paramDef.name] = paramInput;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
docs/js/controls/ControlManager.js
Normal file
116
docs/js/controls/ControlManager.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* ControlManager - Manages UI controls for shapes
|
||||||
|
*
|
||||||
|
* Handles creation, updates, and cleanup of control panels
|
||||||
|
* Ready for Phase 2 multi-shape support (panels per shape)
|
||||||
|
*/
|
||||||
|
class ControlManager {
|
||||||
|
constructor(containerId) {
|
||||||
|
this.container = document.getElementById(containerId);
|
||||||
|
this.filterManager = filterManager; // Use global FilterManager
|
||||||
|
this.factory = new ControlFactory(this);
|
||||||
|
|
||||||
|
// Track all active controls (for future multi-shape, keyed by shape id)
|
||||||
|
this.activeControls = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a filter to a control
|
||||||
|
*/
|
||||||
|
addFilter(controlData, controlConfig, filterType) {
|
||||||
|
const filterInstance = this.filterManager.createFilterInstance(filterType, controlConfig);
|
||||||
|
const filterDef = this.filterManager.getFilter(filterType);
|
||||||
|
|
||||||
|
if (!controlData.filters) {
|
||||||
|
controlData.filters = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create filter UI
|
||||||
|
const filterUI = this.factory.createFilterUI(
|
||||||
|
filterInstance,
|
||||||
|
filterDef,
|
||||||
|
controlData,
|
||||||
|
controlConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
controlData.filtersContainer.appendChild(filterUI);
|
||||||
|
controlData.filters.push(filterInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a filter from a control
|
||||||
|
*/
|
||||||
|
removeFilter(controlData, filterInstance) {
|
||||||
|
const index = controlData.filters.indexOf(filterInstance);
|
||||||
|
if (index > -1) {
|
||||||
|
controlData.filters.splice(index, 1);
|
||||||
|
if (filterInstance.element && filterInstance.element.parentNode) {
|
||||||
|
filterInstance.element.parentNode.removeChild(filterInstance.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a control for a shape
|
||||||
|
* @param {object} config - Control configuration
|
||||||
|
* @param {BaseShape} instance - Shape instance
|
||||||
|
* @returns {object} Control data
|
||||||
|
*/
|
||||||
|
addControl(config, instance) {
|
||||||
|
return this.factory.create(config, instance, this.container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a control
|
||||||
|
* @param {object} controlData - Control data from addControl
|
||||||
|
*/
|
||||||
|
removeControl(controlData) {
|
||||||
|
if (!controlData) return;
|
||||||
|
|
||||||
|
// Remove event listeners
|
||||||
|
if (controlData.element && controlData.listener) {
|
||||||
|
const eventType = controlData.type === 'checkbox' ? 'change' :
|
||||||
|
controlData.type === 'dropdown' ? 'change' : 'input';
|
||||||
|
controlData.element.removeEventListener(eventType, controlData.listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove filters
|
||||||
|
if (controlData.filters) {
|
||||||
|
for (const filter of controlData.filters) {
|
||||||
|
if (filter.element && filter.element.parentNode) {
|
||||||
|
filter.element.parentNode.removeChild(filter.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove wrapper element
|
||||||
|
if (controlData.wrapper && controlData.wrapper.parentNode) {
|
||||||
|
controlData.wrapper.parentNode.removeChild(controlData.wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all controls
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
while (this.container.firstChild) {
|
||||||
|
this.container.removeChild(this.container.firstChild);
|
||||||
|
}
|
||||||
|
this.activeControls.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a control's displayed value programmatically
|
||||||
|
*/
|
||||||
|
updateControlValue(property, value) {
|
||||||
|
const element = document.getElementById(`el${property}`);
|
||||||
|
const label = document.getElementById(`elText${property}`);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.value = value;
|
||||||
|
}
|
||||||
|
if (label) {
|
||||||
|
label.textContent = Math.round(value * 100) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
195
docs/js/controls/FilterManager.js
Normal file
195
docs/js/controls/FilterManager.js
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/**
|
||||||
|
* FilterManager - Manages filter plugins and computes filtered values
|
||||||
|
*
|
||||||
|
* Filters modify control values over time (e.g., oscillate a slider)
|
||||||
|
*/
|
||||||
|
class FilterManager {
|
||||||
|
constructor() {
|
||||||
|
this.filterTypes = new Map();
|
||||||
|
this.registerBuiltinFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register built-in filter types
|
||||||
|
*/
|
||||||
|
registerBuiltinFilters() {
|
||||||
|
// Sine wave filter
|
||||||
|
this.registerFilter('sin', {
|
||||||
|
name: 'Sine Wave',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, rate } = params;
|
||||||
|
const halfRange = (max - min) / 2;
|
||||||
|
return min + halfRange + Math.sin(rate * elapsed * 2) * halfRange;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'rate', type: 'range', label: 'Rate', min: 0.1, max: 10, defaultValue: 1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triangle wave filter
|
||||||
|
this.registerFilter('triangle', {
|
||||||
|
name: 'Triangle Wave',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, rate } = params;
|
||||||
|
const period = (2 * Math.PI) / rate;
|
||||||
|
const t = (elapsed % period) / period;
|
||||||
|
const triangleValue = t < 0.5 ? t * 2 : 2 - t * 2;
|
||||||
|
return min + (max - min) * triangleValue;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'rate', type: 'range', label: 'Rate', min: 0.1, max: 10, defaultValue: 1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sawtooth wave filter
|
||||||
|
this.registerFilter('sawtooth', {
|
||||||
|
name: 'Sawtooth Wave',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, rate } = params;
|
||||||
|
const period = (2 * Math.PI) / rate;
|
||||||
|
const t = (elapsed % period) / period;
|
||||||
|
return min + (max - min) * t;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'rate', type: 'range', label: 'Rate', min: 0.1, max: 10, defaultValue: 1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Square wave filter
|
||||||
|
this.registerFilter('square', {
|
||||||
|
name: 'Square Wave',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, rate } = params;
|
||||||
|
const sinValue = Math.sin(rate * elapsed * 2);
|
||||||
|
return sinValue >= 0 ? max : min;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'rate', type: 'range', label: 'Rate', min: 0.1, max: 10, defaultValue: 1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Random/noise filter
|
||||||
|
this.registerFilter('noise', {
|
||||||
|
name: 'Noise',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, smoothness } = params;
|
||||||
|
// Simple smooth random using sin of elapsed with random-ish multiplier
|
||||||
|
const noise = Math.sin(elapsed * 17.3) * Math.cos(elapsed * 31.7) * 0.5 + 0.5;
|
||||||
|
return min + (max - min) * noise;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'smoothness', type: 'range', label: 'Smoothness', min: 1, max: 100, defaultValue: 50 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
// Linear interpolation (ping-pong)
|
||||||
|
this.registerFilter('linear', {
|
||||||
|
name: 'Linear (Ping-Pong)',
|
||||||
|
compute: (params, elapsed) => {
|
||||||
|
const { min, max, duration } = params;
|
||||||
|
const cycleTime = elapsed % (duration * 2);
|
||||||
|
const t = cycleTime < duration
|
||||||
|
? cycleTime / duration
|
||||||
|
: 1 - (cycleTime - duration) / duration;
|
||||||
|
return min + (max - min) * t;
|
||||||
|
},
|
||||||
|
controls: [
|
||||||
|
{ name: 'min', type: 'range', label: 'Min' },
|
||||||
|
{ name: 'max', type: 'range', label: 'Max' },
|
||||||
|
{ name: 'duration', type: 'range', label: 'Duration (s)', min: 0.5, max: 30, defaultValue: 5 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a custom filter type
|
||||||
|
* @param {string} id - Unique filter identifier
|
||||||
|
* @param {object} filterDef - Filter definition with name, compute, controls
|
||||||
|
*/
|
||||||
|
registerFilter(id, filterDef) {
|
||||||
|
this.filterTypes.set(id, filterDef);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all available filter types
|
||||||
|
* @returns {Array} Array of {id, name} objects
|
||||||
|
*/
|
||||||
|
getAvailableFilters() {
|
||||||
|
const filters = [];
|
||||||
|
for (const [id, def] of this.filterTypes) {
|
||||||
|
filters.push({ id, name: def.name });
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get filter definition by id
|
||||||
|
* @param {string} id - Filter type id
|
||||||
|
* @returns {object|null} Filter definition
|
||||||
|
*/
|
||||||
|
getFilter(id) {
|
||||||
|
return this.filterTypes.get(id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the combined value from multiple filters
|
||||||
|
* @param {Array} filters - Array of active filter instances
|
||||||
|
* @param {number} elapsed - Elapsed time in seconds
|
||||||
|
* @returns {number|null} Computed value or null if no filters
|
||||||
|
*/
|
||||||
|
computeFilteredValue(filters, elapsed) {
|
||||||
|
if (!filters || filters.length === 0) return null;
|
||||||
|
|
||||||
|
let totalValue = 0;
|
||||||
|
for (const filter of filters) {
|
||||||
|
const filterDef = this.getFilter(filter.type);
|
||||||
|
if (filterDef) {
|
||||||
|
totalValue += filterDef.compute(filter.params, elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a filter instance with default params
|
||||||
|
* @param {string} type - Filter type id
|
||||||
|
* @param {object} controlConfig - The control's config (for min/max defaults)
|
||||||
|
* @returns {object} Filter instance
|
||||||
|
*/
|
||||||
|
createFilterInstance(type, controlConfig) {
|
||||||
|
const filterDef = this.getFilter(type);
|
||||||
|
if (!filterDef) {
|
||||||
|
throw new Error(`Unknown filter type: ${type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {};
|
||||||
|
for (const ctrl of filterDef.controls) {
|
||||||
|
if (ctrl.name === 'min') {
|
||||||
|
params.min = controlConfig.min ?? 0;
|
||||||
|
} else if (ctrl.name === 'max') {
|
||||||
|
params.max = controlConfig.max ?? 100;
|
||||||
|
} else {
|
||||||
|
params[ctrl.name] = ctrl.defaultValue ?? 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
params,
|
||||||
|
element: null // Will hold DOM reference for cleanup
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global singleton
|
||||||
|
const filterManager = new FilterManager();
|
||||||
771
docs/js/controls/SceneUI.js
Normal file
771
docs/js/controls/SceneUI.js
Normal file
@@ -0,0 +1,771 @@
|
|||||||
|
/**
|
||||||
|
* SceneUI - Manages the UI for multi-shape scenes
|
||||||
|
*
|
||||||
|
* Creates collapsible panels for each shape with:
|
||||||
|
* - Layer controls (visibility, pause, reorder, remove)
|
||||||
|
* - Shape-specific controls
|
||||||
|
* - Add new shape interface
|
||||||
|
*/
|
||||||
|
class SceneUI {
|
||||||
|
constructor(scene, containerId) {
|
||||||
|
this.scene = scene;
|
||||||
|
this.container = document.getElementById(containerId);
|
||||||
|
this.layerPanels = new Map(); // layerId -> { panel, controlManager }
|
||||||
|
|
||||||
|
this.createSceneControls();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the main scene control area (add shape button, etc.)
|
||||||
|
*/
|
||||||
|
createSceneControls() {
|
||||||
|
// Scene header
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'scene-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<h3>Scene Layers</h3>
|
||||||
|
<div class="scene-header-buttons">
|
||||||
|
<button class="scene-btn" id="add-shape-btn" title="Add Shape">+ Add</button>
|
||||||
|
<button class="scene-btn" id="save-scene-btn" title="Save Scene">💾 Save</button>
|
||||||
|
<button class="scene-btn" id="load-scene-btn" title="Load Scene">📂 Load</button>
|
||||||
|
<button class="scene-btn" id="presets-btn" title="Presets">⭐ Presets</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.container.insertBefore(header, this.container.firstChild);
|
||||||
|
|
||||||
|
// Shape picker modal (hidden by default)
|
||||||
|
this.createShapePicker();
|
||||||
|
|
||||||
|
// Presets modal
|
||||||
|
this.createPresetsModal();
|
||||||
|
|
||||||
|
// Hidden file input for loading
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.id = 'scene-file-input';
|
||||||
|
fileInput.accept = '.json';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
|
||||||
|
// Add shape button handler
|
||||||
|
document.getElementById('add-shape-btn').addEventListener('click', () => {
|
||||||
|
this.showShapePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save button handler
|
||||||
|
document.getElementById('save-scene-btn').addEventListener('click', () => {
|
||||||
|
this.saveScene();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load button handler
|
||||||
|
document.getElementById('load-scene-btn').addEventListener('click', () => {
|
||||||
|
document.getElementById('scene-file-input').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
this.loadScene(e.target.files[0]);
|
||||||
|
e.target.value = ''; // Reset for same file
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Presets button handler
|
||||||
|
document.getElementById('presets-btn').addEventListener('click', () => {
|
||||||
|
this.showPresetsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current scene to file
|
||||||
|
*/
|
||||||
|
saveScene() {
|
||||||
|
const data = this.scene.exportToJSON();
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `scene-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load scene from file
|
||||||
|
*/
|
||||||
|
loadScene(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.target.result);
|
||||||
|
this.scene.importFromJSON(data, this);
|
||||||
|
} catch (err) {
|
||||||
|
alert('Failed to load scene: ' + err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create presets modal
|
||||||
|
*/
|
||||||
|
createPresetsModal() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'presets-modal';
|
||||||
|
modal.id = 'presets-modal';
|
||||||
|
modal.style.display = 'none';
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="presets-content">
|
||||||
|
<h3>Presets</h3>
|
||||||
|
<div class="presets-actions">
|
||||||
|
<button class="preset-save-current-btn">💾 Save Current as Preset</button>
|
||||||
|
</div>
|
||||||
|
<div class="presets-list" id="presets-list">
|
||||||
|
<p class="presets-empty">No presets saved yet</p>
|
||||||
|
</div>
|
||||||
|
<button class="button presets-cancel">Close</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Load and display presets
|
||||||
|
this.refreshPresetsList();
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
modal.querySelector('.presets-cancel').addEventListener('click', () => {
|
||||||
|
this.hidePresetsModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
modal.querySelector('.preset-save-current-btn').addEventListener('click', () => {
|
||||||
|
this.saveCurrentAsPreset();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside to close
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
this.hidePresetsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showPresetsModal() {
|
||||||
|
this.refreshPresetsList();
|
||||||
|
document.getElementById('presets-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
hidePresetsModal() {
|
||||||
|
document.getElementById('presets-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user presets from localStorage
|
||||||
|
*/
|
||||||
|
getPresets() {
|
||||||
|
try {
|
||||||
|
return JSON.parse(localStorage.getItem('animation-presets') || '[]');
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get built-in presets
|
||||||
|
*/
|
||||||
|
getBuiltInPresets() {
|
||||||
|
return typeof BUILT_IN_PRESETS !== 'undefined' ? BUILT_IN_PRESETS : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save presets to localStorage
|
||||||
|
*/
|
||||||
|
savePresets(presets) {
|
||||||
|
localStorage.setItem('animation-presets', JSON.stringify(presets));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save current scene as a preset
|
||||||
|
*/
|
||||||
|
saveCurrentAsPreset() {
|
||||||
|
const name = prompt('Enter preset name:');
|
||||||
|
if (!name) return;
|
||||||
|
|
||||||
|
const presets = this.getPresets();
|
||||||
|
const data = this.scene.exportToJSON();
|
||||||
|
|
||||||
|
presets.push({
|
||||||
|
name: name,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
|
||||||
|
this.savePresets(presets);
|
||||||
|
this.refreshPresetsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a preset (built-in or user)
|
||||||
|
*/
|
||||||
|
loadPreset(index, isBuiltIn = false) {
|
||||||
|
const presets = isBuiltIn ? this.getBuiltInPresets() : this.getPresets();
|
||||||
|
if (index >= 0 && index < presets.length) {
|
||||||
|
this.scene.importFromJSON(presets[index].data, this);
|
||||||
|
this.hidePresetsModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a user preset by index
|
||||||
|
*/
|
||||||
|
deletePreset(index) {
|
||||||
|
const presets = this.getPresets();
|
||||||
|
if (index >= 0 && index < presets.length) {
|
||||||
|
if (confirm(`Delete preset "${presets[index].name}"?`)) {
|
||||||
|
presets.splice(index, 1);
|
||||||
|
this.savePresets(presets);
|
||||||
|
this.refreshPresetsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh the presets list UI
|
||||||
|
*/
|
||||||
|
refreshPresetsList() {
|
||||||
|
const container = document.getElementById('presets-list');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const builtInPresets = this.getBuiltInPresets();
|
||||||
|
const userPresets = this.getPresets();
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
|
||||||
|
// Built-in presets section
|
||||||
|
if (builtInPresets.length > 0) {
|
||||||
|
html += '<div class="presets-section-header">Built-in Presets</div>';
|
||||||
|
html += builtInPresets.map((preset, index) => `
|
||||||
|
<div class="preset-item preset-builtin" data-index="${index}" data-builtin="true">
|
||||||
|
<div class="preset-info">
|
||||||
|
<span class="preset-name">${preset.name}</span>
|
||||||
|
<span class="preset-description">${preset.description || `${preset.data.layers.length} layer(s)`}</span>
|
||||||
|
</div>
|
||||||
|
<div class="preset-actions">
|
||||||
|
<button class="preset-load-btn" data-index="${index}" data-builtin="true">Load</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// User presets section
|
||||||
|
html += '<div class="presets-section-header">My Presets</div>';
|
||||||
|
if (userPresets.length === 0) {
|
||||||
|
html += '<p class="presets-empty">No saved presets yet</p>';
|
||||||
|
} else {
|
||||||
|
html += userPresets.map((preset, index) => `
|
||||||
|
<div class="preset-item" data-index="${index}">
|
||||||
|
<div class="preset-info">
|
||||||
|
<span class="preset-name">${preset.name}</span>
|
||||||
|
<span class="preset-layers">${preset.data.layers.length} layer(s)</span>
|
||||||
|
</div>
|
||||||
|
<div class="preset-actions">
|
||||||
|
<button class="preset-load-btn" data-index="${index}">Load</button>
|
||||||
|
<button class="preset-delete-btn" data-index="${index}">🗑</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
container.querySelectorAll('.preset-load-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const isBuiltIn = btn.dataset.builtin === 'true';
|
||||||
|
this.loadPreset(parseInt(btn.dataset.index), isBuiltIn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll('.preset-delete-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.deletePreset(parseInt(btn.dataset.index));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the shape picker modal with tabs
|
||||||
|
*/
|
||||||
|
createShapePicker() {
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'shape-picker-modal';
|
||||||
|
modal.id = 'shape-picker-modal';
|
||||||
|
modal.style.display = 'none';
|
||||||
|
|
||||||
|
const shapes = shapeRegistry.getAvailableNames();
|
||||||
|
const shapesHtml = shapes.map(name =>
|
||||||
|
`<button class="shape-picker-btn" data-shape="${name}">${name}</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const builtInPresets = this.getBuiltInPresets();
|
||||||
|
const builtInHtml = builtInPresets.map((preset, i) =>
|
||||||
|
`<button class="shape-picker-btn preset-btn" data-preset-index="${i}" data-builtin="true">
|
||||||
|
<span class="preset-btn-name">${preset.name}</span>
|
||||||
|
<span class="preset-btn-info">${preset.data.layers.length} layer(s)</span>
|
||||||
|
</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="shape-picker-container">
|
||||||
|
<h3>Add Layer</h3>
|
||||||
|
<div class="shape-picker-tabs">
|
||||||
|
<button class="shape-picker-tab active" data-tab="shapes">Shapes</button>
|
||||||
|
<button class="shape-picker-tab" data-tab="presets">Presets</button>
|
||||||
|
</div>
|
||||||
|
<div class="shape-picker-content">
|
||||||
|
<div class="shape-picker-panel active" id="panel-shapes">
|
||||||
|
<div class="shape-picker-buttons">
|
||||||
|
${shapesHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shape-picker-panel" id="panel-presets">
|
||||||
|
<div class="shape-picker-section">
|
||||||
|
<div class="shape-picker-section-title">Built-in</div>
|
||||||
|
<div class="shape-picker-buttons">
|
||||||
|
${builtInHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shape-picker-section">
|
||||||
|
<div class="shape-picker-section-title">My Presets</div>
|
||||||
|
<div class="shape-picker-buttons" id="picker-user-presets">
|
||||||
|
<p class="presets-empty">No saved presets</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="shape-picker-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
modal.querySelectorAll('.shape-picker-tab').forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
modal.querySelectorAll('.shape-picker-tab').forEach(t => t.classList.remove('active'));
|
||||||
|
modal.querySelectorAll('.shape-picker-panel').forEach(p => p.classList.remove('active'));
|
||||||
|
tab.classList.add('active');
|
||||||
|
document.getElementById(`panel-${tab.dataset.tab}`).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel button
|
||||||
|
modal.querySelector('.shape-picker-cancel').addEventListener('click', () => {
|
||||||
|
this.hideShapePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shape buttons
|
||||||
|
modal.querySelectorAll('.shape-picker-btn[data-shape]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.addShapeToScene(btn.dataset.shape);
|
||||||
|
this.hideShapePicker();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preset buttons (add as layers, don't replace)
|
||||||
|
modal.querySelectorAll('.preset-btn[data-builtin="true"]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.addPresetAsLayers(parseInt(btn.dataset.presetIndex), true);
|
||||||
|
this.hideShapePicker();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside to close
|
||||||
|
modal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
this.hideShapePicker();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh user presets in the picker
|
||||||
|
*/
|
||||||
|
refreshPickerUserPresets() {
|
||||||
|
const container = document.getElementById('picker-user-presets');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const userPresets = this.getPresets();
|
||||||
|
|
||||||
|
if (userPresets.length === 0) {
|
||||||
|
container.innerHTML = '<p class="presets-empty">No saved presets</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = userPresets.map((preset, i) =>
|
||||||
|
`<button class="shape-picker-btn preset-btn" data-preset-index="${i}" data-builtin="false">
|
||||||
|
<span class="preset-btn-name">${preset.name}</span>
|
||||||
|
<span class="preset-btn-info">${preset.data.layers.length} layer(s)</span>
|
||||||
|
</button>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
container.querySelectorAll('.preset-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
this.addPresetAsLayers(parseInt(btn.dataset.presetIndex), false);
|
||||||
|
this.hideShapePicker();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add preset layers to scene (without clearing existing)
|
||||||
|
*/
|
||||||
|
addPresetAsLayers(index, isBuiltIn) {
|
||||||
|
const presets = isBuiltIn ? this.getBuiltInPresets() : this.getPresets();
|
||||||
|
if (index < 0 || index >= presets.length) return;
|
||||||
|
|
||||||
|
const preset = presets[index];
|
||||||
|
for (const layerData of preset.data.layers) {
|
||||||
|
try {
|
||||||
|
if (!shapeRegistry.get(layerData.name)) {
|
||||||
|
console.warn(`Shape "${layerData.name}" not found, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerId = this.scene.addShape(layerData.name, layerData.values);
|
||||||
|
const layer = this.scene.getLayer(layerId);
|
||||||
|
|
||||||
|
if (!layer || !layer.shape) continue;
|
||||||
|
|
||||||
|
layer.visible = layerData.visible !== false;
|
||||||
|
layer.paused = layerData.paused || false;
|
||||||
|
layer.collapsed = layerData.collapsed !== false;
|
||||||
|
|
||||||
|
// Create panel with filters and bounds
|
||||||
|
this.createLayerPanel(layer, layerData.filters, layerData.controlBounds);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to add layer "${layerData.name}":`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showShapePicker() {
|
||||||
|
this.refreshPickerUserPresets();
|
||||||
|
document.getElementById('shape-picker-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideShapePicker() {
|
||||||
|
document.getElementById('shape-picker-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a shape to the scene and create its UI panel
|
||||||
|
* @param {string} shapeName
|
||||||
|
*/
|
||||||
|
addShapeToScene(shapeName) {
|
||||||
|
const layerId = this.scene.addShape(shapeName);
|
||||||
|
const layer = this.scene.getLayer(layerId);
|
||||||
|
|
||||||
|
// Create the layer panel UI first (it will contain the controls)
|
||||||
|
this.createLayerPanel(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a collapsible panel for a layer
|
||||||
|
* @param {object|number} layerOrId - Layer object or layer ID
|
||||||
|
* @param {object} savedFilters - Optional saved filter settings to restore
|
||||||
|
* @param {object} savedBounds - Optional saved control bounds to restore
|
||||||
|
*/
|
||||||
|
createLayerPanel(layerOrId, savedFilters = null, savedBounds = null) {
|
||||||
|
// Handle both layer object and layer ID
|
||||||
|
const layer = typeof layerOrId === 'number'
|
||||||
|
? this.scene.getLayer(layerOrId)
|
||||||
|
: layerOrId;
|
||||||
|
|
||||||
|
if (!layer || !layer.shape) {
|
||||||
|
console.warn('Cannot create panel: layer or shape is undefined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'layer-panel';
|
||||||
|
panel.id = `layer-panel-${layer.id}`;
|
||||||
|
|
||||||
|
// Layer header with controls
|
||||||
|
const header = document.createElement('div');
|
||||||
|
header.className = 'layer-header';
|
||||||
|
header.innerHTML = `
|
||||||
|
<span class="layer-collapse-btn" data-layer="${layer.id}">▼</span>
|
||||||
|
<span class="layer-name">${layer.name}</span>
|
||||||
|
<div class="layer-controls">
|
||||||
|
<button class="layer-btn layer-visibility" data-layer="${layer.id}" title="Toggle visibility">👁</button>
|
||||||
|
<button class="layer-btn layer-pause" data-layer="${layer.id}" title="Pause/Play">⏸</button>
|
||||||
|
<button class="layer-btn layer-up" data-layer="${layer.id}" title="Move up">↑</button>
|
||||||
|
<button class="layer-btn layer-down" data-layer="${layer.id}" title="Move down">↓</button>
|
||||||
|
<button class="layer-btn layer-remove" data-layer="${layer.id}" title="Remove">✕</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Layer content (shape controls container)
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'layer-content';
|
||||||
|
content.id = `layer-content-${layer.id}`;
|
||||||
|
|
||||||
|
panel.appendChild(header);
|
||||||
|
panel.appendChild(content);
|
||||||
|
|
||||||
|
// Add to layers container
|
||||||
|
const shapesContainer = document.getElementById('layers-container');
|
||||||
|
if (shapesContainer) {
|
||||||
|
shapesContainer.appendChild(panel);
|
||||||
|
} else {
|
||||||
|
this.container.appendChild(panel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a control manager specifically for this layer's content
|
||||||
|
const layerControlManager = new ControlManager(content.id);
|
||||||
|
|
||||||
|
// Initialize the shape's controls into the layer content
|
||||||
|
layer.shape.initializeControls(layerControlManager);
|
||||||
|
|
||||||
|
// Restore saved control bounds if provided
|
||||||
|
if (savedBounds) {
|
||||||
|
this.applyBoundsToShape(layer.shape, savedBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore saved filters if provided
|
||||||
|
if (savedFilters) {
|
||||||
|
this.applyFiltersToShape(layer.shape, layerControlManager, savedFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store both panel and control manager
|
||||||
|
this.layerPanels.set(layer.id, { panel, controlManager: layerControlManager });
|
||||||
|
|
||||||
|
// Attach event handlers
|
||||||
|
this.attachLayerHandlers(layer.id, panel);
|
||||||
|
|
||||||
|
// Apply collapsed state
|
||||||
|
if (layer.collapsed) {
|
||||||
|
this.updateLayerUI(layer.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply saved filters to a shape's controls
|
||||||
|
*/
|
||||||
|
applyFiltersToShape(shape, controlManager, savedFilters) {
|
||||||
|
if (!savedFilters || !shape.controls) return;
|
||||||
|
|
||||||
|
for (const control of shape.controls) {
|
||||||
|
// Property is stored in control.config.property
|
||||||
|
const property = control.config?.property;
|
||||||
|
if (!property || !savedFilters[property]) continue;
|
||||||
|
|
||||||
|
const filterDataArray = savedFilters[property];
|
||||||
|
const controlConfig = control.config || {};
|
||||||
|
|
||||||
|
for (const filterData of filterDataArray) {
|
||||||
|
// Create filter instance with saved params
|
||||||
|
const filterInstance = {
|
||||||
|
type: filterData.type,
|
||||||
|
params: { ...filterData.params }
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterDef = controlManager.filterManager.getFilter(filterData.type);
|
||||||
|
if (!filterDef) continue;
|
||||||
|
|
||||||
|
// Create filter UI
|
||||||
|
const filterUI = controlManager.factory.createFilterUI(
|
||||||
|
filterInstance,
|
||||||
|
filterDef,
|
||||||
|
control,
|
||||||
|
controlConfig
|
||||||
|
);
|
||||||
|
|
||||||
|
if (control.filtersContainer) {
|
||||||
|
control.filtersContainer.appendChild(filterUI);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!control.filters) {
|
||||||
|
control.filters = [];
|
||||||
|
}
|
||||||
|
control.filters.push(filterInstance);
|
||||||
|
|
||||||
|
// Update filter badge
|
||||||
|
controlManager.factory.updateFilterBadge(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply saved control bounds to a shape's controls
|
||||||
|
*/
|
||||||
|
applyBoundsToShape(shape, savedBounds) {
|
||||||
|
if (!savedBounds || !shape.controls) return;
|
||||||
|
|
||||||
|
for (const control of shape.controls) {
|
||||||
|
const property = control.config?.property;
|
||||||
|
if (!property || !savedBounds[property]) continue;
|
||||||
|
|
||||||
|
const bounds = savedBounds[property];
|
||||||
|
|
||||||
|
// Update configState
|
||||||
|
if (control.configState) {
|
||||||
|
control.configState.min = bounds.min;
|
||||||
|
control.configState.max = bounds.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the input element's min/max
|
||||||
|
if (control.element) {
|
||||||
|
control.element.min = bounds.min;
|
||||||
|
control.element.max = bounds.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the settings panel inputs if they exist
|
||||||
|
if (control.settingsPanel) {
|
||||||
|
const inputs = control.settingsPanel.querySelectorAll('.setting-input');
|
||||||
|
if (inputs[0]) inputs[0].value = bounds.min;
|
||||||
|
if (inputs[1]) inputs[1].value = bounds.max;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach event handlers for layer controls
|
||||||
|
*/
|
||||||
|
attachLayerHandlers(layerId, panel) {
|
||||||
|
// Collapse toggle
|
||||||
|
panel.querySelector('.layer-collapse-btn').addEventListener('click', () => {
|
||||||
|
this.toggleCollapse(layerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Visibility toggle
|
||||||
|
panel.querySelector('.layer-visibility').addEventListener('click', () => {
|
||||||
|
this.scene.toggleVisibility(layerId);
|
||||||
|
this.updateLayerUI(layerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pause toggle
|
||||||
|
panel.querySelector('.layer-pause').addEventListener('click', () => {
|
||||||
|
this.scene.togglePause(layerId);
|
||||||
|
this.updateLayerUI(layerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move up
|
||||||
|
panel.querySelector('.layer-up').addEventListener('click', () => {
|
||||||
|
this.scene.moveUp(layerId);
|
||||||
|
this.reorderPanels();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move down
|
||||||
|
panel.querySelector('.layer-down').addEventListener('click', () => {
|
||||||
|
this.scene.moveDown(layerId);
|
||||||
|
this.reorderPanels();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
panel.querySelector('.layer-remove').addEventListener('click', () => {
|
||||||
|
this.removeLayer(layerId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle collapse state for a layer panel
|
||||||
|
*/
|
||||||
|
toggleCollapse(layerId) {
|
||||||
|
const layer = this.scene.getLayer(layerId);
|
||||||
|
if (!layer) return;
|
||||||
|
|
||||||
|
layer.collapsed = !layer.collapsed;
|
||||||
|
this.updateLayerUI(layerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update layer UI to reflect current state
|
||||||
|
*/
|
||||||
|
updateLayerUI(layerId) {
|
||||||
|
const layer = this.scene.getLayer(layerId);
|
||||||
|
const layerData = this.layerPanels.get(layerId);
|
||||||
|
if (!layer || !layerData) return;
|
||||||
|
|
||||||
|
const panel = layerData.panel;
|
||||||
|
|
||||||
|
// Update collapse
|
||||||
|
const collapseBtn = panel.querySelector('.layer-collapse-btn');
|
||||||
|
const content = panel.querySelector('.layer-content');
|
||||||
|
if (layer.collapsed) {
|
||||||
|
collapseBtn.textContent = '▶';
|
||||||
|
content.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
collapseBtn.textContent = '▼';
|
||||||
|
content.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update visibility button
|
||||||
|
const visBtn = panel.querySelector('.layer-visibility');
|
||||||
|
visBtn.style.opacity = layer.visible ? '1' : '0.4';
|
||||||
|
|
||||||
|
// Update pause button
|
||||||
|
const pauseBtn = panel.querySelector('.layer-pause');
|
||||||
|
pauseBtn.textContent = layer.paused ? '▶' : '⏸';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reorder panels to match scene layer order
|
||||||
|
*/
|
||||||
|
reorderPanels() {
|
||||||
|
const container = document.getElementById('layers-container') || this.container;
|
||||||
|
|
||||||
|
for (const layer of this.scene.getLayers()) {
|
||||||
|
const layerData = this.layerPanels.get(layer.id);
|
||||||
|
if (layerData && layerData.panel) {
|
||||||
|
container.appendChild(layerData.panel); // Moves to end, in order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a layer and its panel
|
||||||
|
*/
|
||||||
|
removeLayer(layerId) {
|
||||||
|
const layerData = this.layerPanels.get(layerId);
|
||||||
|
|
||||||
|
// Clean up control manager
|
||||||
|
if (layerData && layerData.controlManager) {
|
||||||
|
layerData.controlManager.clearAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from scene
|
||||||
|
this.scene.removeShape(layerId);
|
||||||
|
|
||||||
|
// Remove panel from DOM
|
||||||
|
if (layerData && layerData.panel && layerData.panel.parentNode) {
|
||||||
|
layerData.panel.parentNode.removeChild(layerData.panel);
|
||||||
|
}
|
||||||
|
this.layerPanels.delete(layerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all layer panels (for import)
|
||||||
|
*/
|
||||||
|
clearAllPanels() {
|
||||||
|
for (const [layerId, layerData] of this.layerPanels) {
|
||||||
|
if (layerData.controlManager) {
|
||||||
|
layerData.controlManager.clearAll();
|
||||||
|
}
|
||||||
|
if (layerData.panel && layerData.panel.parentNode) {
|
||||||
|
layerData.panel.parentNode.removeChild(layerData.panel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.layerPanels.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all layer panels and scene
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
this.clearAllPanels();
|
||||||
|
this.scene.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
163
docs/js/core/AnimationEngine.js
Normal file
163
docs/js/core/AnimationEngine.js
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
/**
|
||||||
|
* AnimationEngine - Manages the canvas, render loop, and timing
|
||||||
|
*/
|
||||||
|
class AnimationEngine {
|
||||||
|
constructor(canvasId) {
|
||||||
|
this.canvas = document.getElementById(canvasId);
|
||||||
|
this.ctx = this.canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Sizing
|
||||||
|
this.resizeCanvas();
|
||||||
|
window.addEventListener('resize', () => this.resizeCanvas());
|
||||||
|
|
||||||
|
// Timing
|
||||||
|
this.targetFps = 60;
|
||||||
|
this.frameDuration = 1000 / this.targetFps;
|
||||||
|
this.lastTimestamp = 0;
|
||||||
|
this.elapsedTime = 0;
|
||||||
|
this.rotation = 0;
|
||||||
|
this.degPerSec = 10;
|
||||||
|
|
||||||
|
// State
|
||||||
|
this.paused = false;
|
||||||
|
this.isRunning = false;
|
||||||
|
|
||||||
|
// Scene reference (set via setScene)
|
||||||
|
this.scene = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the scene to render
|
||||||
|
* @param {Scene} scene - The scene instance
|
||||||
|
*/
|
||||||
|
setScene(scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resize canvas to fill window
|
||||||
|
*/
|
||||||
|
resizeCanvas() {
|
||||||
|
this.canvas.width = window.innerWidth;
|
||||||
|
this.canvas.height = window.innerHeight;
|
||||||
|
this.centerX = this.canvas.width / 2;
|
||||||
|
this.centerY = this.canvas.height / 2;
|
||||||
|
|
||||||
|
// Update global references for backward compatibility
|
||||||
|
if (typeof centerX !== 'undefined') {
|
||||||
|
centerX = this.centerX;
|
||||||
|
centerY = this.centerY;
|
||||||
|
}
|
||||||
|
if (typeof ctx !== 'undefined') {
|
||||||
|
ctx = this.ctx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the canvas
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
this.ctx.fillStyle = 'black';
|
||||||
|
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main render loop
|
||||||
|
*/
|
||||||
|
render(timestamp) {
|
||||||
|
if (!this.isRunning) return;
|
||||||
|
|
||||||
|
if (!this.lastTimestamp) {
|
||||||
|
this.lastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaTime = timestamp - this.lastTimestamp;
|
||||||
|
this.lastTimestamp = timestamp;
|
||||||
|
|
||||||
|
let adjustedDeltaTime = 0;
|
||||||
|
|
||||||
|
if (!this.paused) {
|
||||||
|
this.rotation += this.degPerSec / this.targetFps;
|
||||||
|
this.elapsedTime += deltaTime;
|
||||||
|
adjustedDeltaTime = deltaTime / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustedElapsed = this.elapsedTime / 1000;
|
||||||
|
|
||||||
|
// Clear and draw
|
||||||
|
this.clear();
|
||||||
|
if (this.scene) {
|
||||||
|
this.scene.draw(adjustedElapsed, adjustedDeltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame((ts) => this.render(ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the animation loop
|
||||||
|
*/
|
||||||
|
start() {
|
||||||
|
if (this.isRunning) return;
|
||||||
|
this.isRunning = true;
|
||||||
|
this.lastTimestamp = 0;
|
||||||
|
requestAnimationFrame((ts) => this.render(ts));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the animation loop
|
||||||
|
*/
|
||||||
|
stop() {
|
||||||
|
this.isRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle pause state
|
||||||
|
*/
|
||||||
|
togglePause() {
|
||||||
|
this.paused = !this.paused;
|
||||||
|
return this.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset rotation/timing
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.rotation = 0;
|
||||||
|
this.elapsedTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step forward one frame
|
||||||
|
*/
|
||||||
|
stepForward() {
|
||||||
|
this.rotation += this.degPerSec / this.targetFps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Step backward one frame
|
||||||
|
*/
|
||||||
|
stepBackward() {
|
||||||
|
this.rotation -= this.degPerSec / this.targetFps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set degrees per second
|
||||||
|
*/
|
||||||
|
setSpeed(degPerSec) {
|
||||||
|
this.degPerSec = parseFloat(degPerSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get canvas context (for shapes that need direct access)
|
||||||
|
*/
|
||||||
|
getContext() {
|
||||||
|
return this.ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get center coordinates
|
||||||
|
*/
|
||||||
|
getCenter() {
|
||||||
|
return { x: this.centerX, y: this.centerY };
|
||||||
|
}
|
||||||
|
}
|
||||||
124
docs/js/core/BaseShape.js
Normal file
124
docs/js/core/BaseShape.js
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/**
|
||||||
|
* BaseShape - Base class for all animated shapes
|
||||||
|
*
|
||||||
|
* To create a new shape:
|
||||||
|
* 1. Create a new file in /js/shapes/
|
||||||
|
* 2. Extend BaseShape
|
||||||
|
* 3. Define static `config` array with control definitions
|
||||||
|
* 4. Implement constructor with matching parameters
|
||||||
|
* 5. Implement `draw(elapsed, deltaTime)` method
|
||||||
|
* 6. Register with shapeRegistry at the end of the file
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* ```
|
||||||
|
* class MyShape extends BaseShape {
|
||||||
|
* static config = [
|
||||||
|
* { type: 'range', property: 'size', min: 10, max: 500, defaultValue: 100 },
|
||||||
|
* { type: 'color', property: 'color', defaultValue: '#ff0000' }
|
||||||
|
* ];
|
||||||
|
*
|
||||||
|
* constructor(size, color) {
|
||||||
|
* super();
|
||||||
|
* this.size = size;
|
||||||
|
* this.color = color;
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* draw(elapsed, deltaTime) {
|
||||||
|
* this.updateFilters(elapsed);
|
||||||
|
* // Drawing code using this.size and this.color
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* shapeRegistry.register('MyShape', MyShape);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* Control types:
|
||||||
|
* - range: { type, property, min, max, defaultValue, callback? }
|
||||||
|
* - color: { type, property, defaultValue }
|
||||||
|
* - checkbox: { type, property, defaultValue }
|
||||||
|
* - dropdown: { type, property, defaultValue, options: [{value, label}] }
|
||||||
|
* - button: { type, label, method }
|
||||||
|
* - header: { type, text }
|
||||||
|
*/
|
||||||
|
class BaseShape {
|
||||||
|
constructor() {
|
||||||
|
this.controls = [];
|
||||||
|
this.controlManager = null;
|
||||||
|
this.speedMultiplier = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize controls using the ControlManager
|
||||||
|
* Called automatically when a shape is created
|
||||||
|
* @param {ControlManager} controlManager - The control manager instance
|
||||||
|
*/
|
||||||
|
initializeControls(controlManager) {
|
||||||
|
this.controlManager = controlManager;
|
||||||
|
|
||||||
|
// Get config from static property
|
||||||
|
const config = this.constructor.config || [];
|
||||||
|
|
||||||
|
for (const item of config) {
|
||||||
|
const controlData = controlManager.addControl(item, this);
|
||||||
|
if (controlData) {
|
||||||
|
this.controls.push(controlData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add speed multiplier control (common to all shapes)
|
||||||
|
const speedControl = controlManager.addControl({
|
||||||
|
type: 'range',
|
||||||
|
min: 1,
|
||||||
|
max: 1000,
|
||||||
|
defaultValue: this.speedMultiplier,
|
||||||
|
property: 'speedMultiplier'
|
||||||
|
}, this);
|
||||||
|
if (speedControl) {
|
||||||
|
this.controls.push(speedControl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up controls when shape is removed
|
||||||
|
*/
|
||||||
|
remove() {
|
||||||
|
if (this.controlManager) {
|
||||||
|
for (const control of this.controls) {
|
||||||
|
this.controlManager.removeControl(control);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.controls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update any active filters on controls
|
||||||
|
* Call this at the start of your draw() method
|
||||||
|
* @param {number} elapsed - Total elapsed time in seconds
|
||||||
|
*/
|
||||||
|
updateFilters(elapsed) {
|
||||||
|
if (!this.controlManager) return;
|
||||||
|
|
||||||
|
for (const control of this.controls) {
|
||||||
|
if (control.filters && control.filters.length > 0) {
|
||||||
|
const newValue = this.controlManager.filterManager.computeFilteredValue(
|
||||||
|
control.filters,
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newValue !== null && control.element) {
|
||||||
|
control.element.value = newValue;
|
||||||
|
const event = new Event('input', { bubbles: true });
|
||||||
|
control.element.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main draw method - override in subclasses
|
||||||
|
* @param {number} elapsed - Total elapsed time in seconds
|
||||||
|
* @param {number} deltaTime - Time since last frame
|
||||||
|
*/
|
||||||
|
draw(elapsed, deltaTime) {
|
||||||
|
throw new Error('draw() method not implemented');
|
||||||
|
}
|
||||||
|
}
|
||||||
267
docs/js/core/Scene.js
Normal file
267
docs/js/core/Scene.js
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
/**
|
||||||
|
* Scene - Manages multiple shapes on a single canvas
|
||||||
|
*
|
||||||
|
* Features:
|
||||||
|
* - Multiple shapes rendered in layer order
|
||||||
|
* - Per-shape visibility and pause controls
|
||||||
|
* - Collapsible control panels per shape
|
||||||
|
* - Add/remove shapes dynamically
|
||||||
|
*/
|
||||||
|
class Scene {
|
||||||
|
constructor() {
|
||||||
|
this.layers = []; // Array of { id, shape, visible, paused, collapsed }
|
||||||
|
this.nextLayerId = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a shape to the scene
|
||||||
|
* @param {string} shapeName - Name of the shape class
|
||||||
|
* @param {object} initialValues - Initial property values
|
||||||
|
* @returns {number} Layer ID
|
||||||
|
*/
|
||||||
|
addShape(shapeName, initialValues = {}) {
|
||||||
|
const shape = shapeRegistry.createInstance(shapeName, initialValues);
|
||||||
|
|
||||||
|
if (!shape) {
|
||||||
|
throw new Error(`Failed to create shape: ${shapeName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const layer = {
|
||||||
|
id: this.nextLayerId++,
|
||||||
|
name: shapeName,
|
||||||
|
shape: shape,
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.layers.push(layer);
|
||||||
|
return layer.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a shape from the scene
|
||||||
|
* @param {number} layerId - Layer ID to remove
|
||||||
|
*/
|
||||||
|
removeShape(layerId) {
|
||||||
|
const index = this.layers.findIndex(l => l.id === layerId);
|
||||||
|
if (index !== -1) {
|
||||||
|
const layer = this.layers[index];
|
||||||
|
// Clean up controls
|
||||||
|
if (layer.shape.remove) {
|
||||||
|
layer.shape.remove();
|
||||||
|
}
|
||||||
|
this.layers.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a layer by ID
|
||||||
|
* @param {number} layerId
|
||||||
|
* @returns {object|null}
|
||||||
|
*/
|
||||||
|
getLayer(layerId) {
|
||||||
|
return this.layers.find(l => l.id === layerId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle layer visibility
|
||||||
|
* @param {number} layerId
|
||||||
|
*/
|
||||||
|
toggleVisibility(layerId) {
|
||||||
|
const layer = this.getLayer(layerId);
|
||||||
|
if (layer) {
|
||||||
|
layer.visible = !layer.visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle layer pause state
|
||||||
|
* @param {number} layerId
|
||||||
|
*/
|
||||||
|
togglePause(layerId) {
|
||||||
|
const layer = this.getLayer(layerId);
|
||||||
|
if (layer) {
|
||||||
|
layer.paused = !layer.paused;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle layer control panel collapsed state
|
||||||
|
* @param {number} layerId
|
||||||
|
*/
|
||||||
|
toggleCollapsed(layerId) {
|
||||||
|
const layer = this.getLayer(layerId);
|
||||||
|
if (layer) {
|
||||||
|
layer.collapsed = !layer.collapsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a layer up in the render order
|
||||||
|
* @param {number} layerId
|
||||||
|
*/
|
||||||
|
moveUp(layerId) {
|
||||||
|
const index = this.layers.findIndex(l => l.id === layerId);
|
||||||
|
if (index > 0) {
|
||||||
|
[this.layers[index - 1], this.layers[index]] = [this.layers[index], this.layers[index - 1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a layer down in the render order
|
||||||
|
* @param {number} layerId
|
||||||
|
*/
|
||||||
|
moveDown(layerId) {
|
||||||
|
const index = this.layers.findIndex(l => l.id === layerId);
|
||||||
|
if (index < this.layers.length - 1) {
|
||||||
|
[this.layers[index], this.layers[index + 1]] = [this.layers[index + 1], this.layers[index]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw all visible shapes
|
||||||
|
* @param {number} elapsed - Total elapsed time
|
||||||
|
* @param {number} deltaTime - Time since last frame
|
||||||
|
*/
|
||||||
|
draw(elapsed, deltaTime) {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
if (layer.visible && !layer.paused) {
|
||||||
|
layer.shape.draw(elapsed, deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all layers
|
||||||
|
* @returns {array}
|
||||||
|
*/
|
||||||
|
getLayers() {
|
||||||
|
return this.layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all layers
|
||||||
|
*/
|
||||||
|
clear() {
|
||||||
|
for (const layer of this.layers) {
|
||||||
|
if (layer.shape.remove) {
|
||||||
|
layer.shape.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.layers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export scene to JSON
|
||||||
|
* @returns {object} Scene data
|
||||||
|
*/
|
||||||
|
exportToJSON() {
|
||||||
|
return {
|
||||||
|
version: 1,
|
||||||
|
layers: this.layers.map(layer => {
|
||||||
|
const shape = layer.shape;
|
||||||
|
const ShapeClass = shape.constructor;
|
||||||
|
const config = ShapeClass.config || [];
|
||||||
|
|
||||||
|
// Extract current values from shape
|
||||||
|
const values = {};
|
||||||
|
config.forEach(item => {
|
||||||
|
if (item.property && shape.hasOwnProperty(item.property)) {
|
||||||
|
values[item.property] = shape[item.property];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also save speedMultiplier
|
||||||
|
values.speedMultiplier = shape.speedMultiplier;
|
||||||
|
|
||||||
|
// Extract filter settings and control bounds from shape.controls
|
||||||
|
const filters = {};
|
||||||
|
const controlBounds = {};
|
||||||
|
if (shape.controls) {
|
||||||
|
for (const control of shape.controls) {
|
||||||
|
const property = control.config?.property;
|
||||||
|
if (!property) continue;
|
||||||
|
|
||||||
|
// Save filters
|
||||||
|
if (control.filters && control.filters.length > 0) {
|
||||||
|
filters[property] = control.filters.map(f => ({
|
||||||
|
type: f.type,
|
||||||
|
params: { ...f.params }
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save custom min/max bounds if different from original
|
||||||
|
if (control.configState) {
|
||||||
|
const { min, max, originalMin, originalMax } = control.configState;
|
||||||
|
if (min !== originalMin || max !== originalMax) {
|
||||||
|
controlBounds[property] = { min, max };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: layer.name,
|
||||||
|
visible: layer.visible,
|
||||||
|
paused: layer.paused,
|
||||||
|
collapsed: layer.collapsed,
|
||||||
|
values: values,
|
||||||
|
filters: filters,
|
||||||
|
controlBounds: controlBounds
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import scene from JSON
|
||||||
|
* @param {object} data - Scene data
|
||||||
|
* @param {SceneUI} sceneUI - UI instance to rebuild panels
|
||||||
|
*/
|
||||||
|
importFromJSON(data, sceneUI) {
|
||||||
|
// Clear existing
|
||||||
|
this.clear();
|
||||||
|
if (sceneUI) {
|
||||||
|
sceneUI.clearAllPanels();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate version
|
||||||
|
if (!data.version || !data.layers) {
|
||||||
|
throw new Error('Invalid scene data');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate layers
|
||||||
|
for (const layerData of data.layers) {
|
||||||
|
try {
|
||||||
|
// Check if shape type exists
|
||||||
|
if (!shapeRegistry.get(layerData.name)) {
|
||||||
|
console.warn(`Shape "${layerData.name}" not found, skipping`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerId = this.addShape(layerData.name, layerData.values);
|
||||||
|
const layer = this.getLayer(layerId);
|
||||||
|
|
||||||
|
if (!layer || !layer.shape) {
|
||||||
|
console.warn(`Failed to create layer for "${layerData.name}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.visible = layerData.visible !== false;
|
||||||
|
layer.paused = layerData.paused || false;
|
||||||
|
layer.collapsed = layerData.collapsed || false;
|
||||||
|
|
||||||
|
// Create UI panel (this also creates controls, applies filters and bounds)
|
||||||
|
if (sceneUI) {
|
||||||
|
sceneUI.createLayerPanel(layer, layerData.filters, layerData.controlBounds);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load shape "${layerData.name}":`, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global scene instance
|
||||||
|
const scene = new Scene();
|
||||||
89
docs/js/core/ShapeRegistry.js
Normal file
89
docs/js/core/ShapeRegistry.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* ShapeRegistry - Automatically registers and manages available shape classes
|
||||||
|
* Shapes register themselves when loaded, no manual map needed
|
||||||
|
*/
|
||||||
|
class ShapeRegistry {
|
||||||
|
constructor() {
|
||||||
|
this.shapes = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a shape class
|
||||||
|
* @param {string} name - The name of the shape
|
||||||
|
* @param {class} shapeClass - The shape class constructor
|
||||||
|
*/
|
||||||
|
register(name, shapeClass) {
|
||||||
|
if (this.shapes.has(name)) {
|
||||||
|
console.warn(`Shape "${name}" is already registered. Overwriting.`);
|
||||||
|
}
|
||||||
|
this.shapes.set(name, shapeClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a shape class by name
|
||||||
|
* @param {string} name - The name of the shape
|
||||||
|
* @returns {class|null} The shape class or null if not found
|
||||||
|
*/
|
||||||
|
get(name) {
|
||||||
|
return this.shapes.get(name) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an instance of a shape
|
||||||
|
* @param {string} name - The name of the shape
|
||||||
|
* @param {object} initialValues - Initial property values (overrides defaults)
|
||||||
|
* @returns {BaseShape} The shape instance
|
||||||
|
*/
|
||||||
|
createInstance(name, initialValues = {}) {
|
||||||
|
const ShapeClass = this.get(name);
|
||||||
|
if (!ShapeClass) {
|
||||||
|
throw new Error(`Unknown shape: "${name}". Available shapes: ${this.getAvailableNames().join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the static config to determine constructor argument order
|
||||||
|
const config = ShapeClass.config || [];
|
||||||
|
|
||||||
|
// Build positional arguments array from config defaults + initialValues overrides
|
||||||
|
const args = config
|
||||||
|
.filter(item => item.property) // Only items with properties
|
||||||
|
.map(item => {
|
||||||
|
// Use provided value or fall back to default
|
||||||
|
return initialValues.hasOwnProperty(item.property)
|
||||||
|
? initialValues[item.property]
|
||||||
|
: item.defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ShapeClass(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all registered shape names (alphabetically sorted)
|
||||||
|
* @returns {string[]} Array of shape names
|
||||||
|
*/
|
||||||
|
getAvailableNames() {
|
||||||
|
return Array.from(this.shapes.keys()).sort((a, b) => a.localeCompare(b));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the config definition for a shape
|
||||||
|
* @param {string} name - The name of the shape
|
||||||
|
* @returns {array|null} The config array or null
|
||||||
|
*/
|
||||||
|
getConfig(name) {
|
||||||
|
const ShapeClass = this.get(name);
|
||||||
|
if (!ShapeClass) return null;
|
||||||
|
return ShapeClass.config || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a shape is registered
|
||||||
|
* @param {string} name - The name of the shape
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
has(name) {
|
||||||
|
return this.shapes.has(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global singleton instance
|
||||||
|
const shapeRegistry = new ShapeRegistry();
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
async function fetchConfig(className) {
|
|
||||||
// const config = await $.getJSON("config.json");
|
|
||||||
const config = {
|
|
||||||
PolyTwistColourWidth: [
|
|
||||||
{ type: "range", min: 3, max: 10, defaultValue: 5, property: "sides" },
|
|
||||||
{ type: "range", min: 400, max: 2000, defaultValue: 400, property: "width" },
|
|
||||||
{ type: "range", min: 2, max: 5, defaultValue: 5, property: "line_width" },
|
|
||||||
{ type: "range", min: 1, max: 100, defaultValue: 50, property: "depth" },
|
|
||||||
{ type: "range", min: -180, max: 180, defaultValue: -90, property: "rotation", },
|
|
||||||
{ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier", },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#42f57b", property: "colour2" },
|
|
||||||
],
|
|
||||||
FloralPhyllo: [
|
|
||||||
{ type: "range", min: 1, max: 600, defaultValue: 300, property: "width" },
|
|
||||||
{ type: "range", min: 1, max: 300, defaultValue: 150, property: "depth" },
|
|
||||||
{ type: "range", min: 0, max: 3141, defaultValue: 0, property: "start" },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#FC0362", property: "colour2" },
|
|
||||||
],
|
|
||||||
Spiral1: [
|
|
||||||
{ type: "range", min: 1, max: 50, defaultValue: 20, property: "sides" },
|
|
||||||
{ type: "range", min: 1, max: 600, defaultValue: 240, property: "width" },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour" },
|
|
||||||
],
|
|
||||||
FloralAccident: [
|
|
||||||
{ type: "range", min: 1, max: 50, defaultValue: 20, property: "sides" },
|
|
||||||
{ type: "range", min: 1, max: 600, defaultValue: 240, property: "width" },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour" },
|
|
||||||
],
|
|
||||||
FloralPhyllo_Accident: [
|
|
||||||
{ type: "range", min: 1, max: 50, defaultValue: 20, property: "sides" },
|
|
||||||
{ type: "range", min: 1, max: 600, defaultValue: 240, property: "width" },
|
|
||||||
{ type: "color", defaultValue: "#2D81FC", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#FC0362", property: "colour2" },
|
|
||||||
],
|
|
||||||
Nodal_expanding: [
|
|
||||||
{ type: "range", min: 1, max: 100, defaultValue: 5, property: "expand" },
|
|
||||||
{ type: "range", min: 1, max: 1000, defaultValue: 150, property: "points" },
|
|
||||||
{ type: "range", min: 1, max: 360, defaultValue: 0, property: "start" },
|
|
||||||
{ type: "range", min: 1, max: 10, defaultValue: 6, property: "line_width" },
|
|
||||||
{ type: "color", defaultValue: "#2D81FC", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#FC0362", property: "colour2" },
|
|
||||||
{ type: "range", min: 0, max: 10, defaultValue: 5, property: "colour_change" },
|
|
||||||
],
|
|
||||||
Phyllotaxis: [
|
|
||||||
{ type: "range", min: 1, max: 40, defaultValue: 24, property: "width" },
|
|
||||||
{ type: "range", min: 1, max: 40, defaultValue: 10, property: "size" },
|
|
||||||
{ type: "range", min: 1, max: 40, defaultValue: 4, property: "sizeMin" },
|
|
||||||
{ type: "range", min: 0, max: 3141, defaultValue: 0, property: "start" },
|
|
||||||
{ type: "range", min: 1, max: 10000, defaultValue: 300, property: "nMax" },
|
|
||||||
{ type: "range", min: 0, max: 2, defaultValue: 0, property: "wave" },
|
|
||||||
{ type: "range", min: 1, max: 12, defaultValue: 2, property: "spiralProngs" },
|
|
||||||
{ type: "color", defaultValue: "#2D81FC", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#FC0362", property: "colour2" },
|
|
||||||
],
|
|
||||||
SquareTwist_angle: [
|
|
||||||
{ type: "range", min: 1, max: 800, defaultValue: 400, property: "width" },
|
|
||||||
{ type: "range", min: 1, max: 10, defaultValue: 1, property: "line_width" },
|
|
||||||
{ type: "color", defaultValue: "#2D81FC", property: "colour1" },
|
|
||||||
],
|
|
||||||
EyePrototype: [
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: 0, property: "x" },
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: 0, property: "y" },
|
|
||||||
{ type: "range", min: -180, max: 180, defaultValue: 0, property: "rotate" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "flip" },
|
|
||||||
{ type: "range", min: 1, max: 800, defaultValue: 400, property: "width" },
|
|
||||||
{ type: "range", min: 1, max: 100, defaultValue: 5, property: "blink_speed" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 0, property: "draw_spiral" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "spiral_full" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 0, property: "draw_pupil" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 0, property: "draw_expand" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "draw_hypno" },
|
|
||||||
{ type: "range", min: 1, max: 10, defaultValue: 1, property: "line_width" },
|
|
||||||
{ type: "color", defaultValue: "#00fffb", property: "colourPupil" },
|
|
||||||
{ type: "color", defaultValue: "#ff0000", property: "colourSpiral" },
|
|
||||||
{ type: "color", defaultValue: "#00fffb", property: "colourExpand" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "draw_eyelid" },
|
|
||||||
],
|
|
||||||
CircleExpand: [
|
|
||||||
{ type: "range", min: 1, max: 70, defaultValue: 21, property: "nCircles" },
|
|
||||||
{ type: "range", min: 50, max: 150, defaultValue: 150, property: "gap" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "linear" },
|
|
||||||
{ type: "range", min: 0, max: 1, defaultValue: 1, property: "heart" },
|
|
||||||
{ type: "color", defaultValue: "#fc03cf", property: "colour1" },
|
|
||||||
{ type: "color", defaultValue: "#00fffb", property: "colour2" },
|
|
||||||
],
|
|
||||||
MaryFace: [
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: -110, property: "x1" },
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: -140, property: "y1" },
|
|
||||||
{ type: "range", min: -180, max: 180, defaultValue: 18, property: "rotate1" },
|
|
||||||
{ type: "range", min: 0, max: 400, defaultValue: 160, property: "width1" },
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: 195, property: "x2" },
|
|
||||||
{ type: "range", min: -400, max: 400, defaultValue: -30, property: "y2" },
|
|
||||||
{ type: "range", min: -180, max: 180, defaultValue: 18, property: "rotate2" },
|
|
||||||
{ type: "range", min: 0, max: 400, defaultValue: 160, property: "width2" },
|
|
||||||
],
|
|
||||||
Countdown: [
|
|
||||||
{ type: "range", min: 8000, max: 2000000, defaultValue: 2000000, property: "milestone" },
|
|
||||||
],
|
|
||||||
NewWave: [
|
|
||||||
{ type: "range", min: 300, max: 1600, defaultValue: 342, property: "width" },
|
|
||||||
{ type: "range", min: 2, max: 40, defaultValue: 4, property: "sides" },
|
|
||||||
{ type: "range", min: 1, max: 100, defaultValue: 1, property: "step" },
|
|
||||||
{ type: "range", min: 1, max: 10, defaultValue: 4, property: "lineWidth" },
|
|
||||||
{ type: "range", min: 100, max: 1000, defaultValue: 100, property: "limiter" },
|
|
||||||
],
|
|
||||||
RaysInShape: [
|
|
||||||
{ type: "range", min: 50, max: 1000, defaultValue: 500, property: "rays", callback: (instance, newValue) => instance.setRays(newValue) },
|
|
||||||
{ type: "range", min: 1, max: 30, defaultValue: 2, property: "speed" },
|
|
||||||
{ type: "checkbox", defaultValue: true, property: "doesWave" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVertRate" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorrRate" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVert" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorr" },
|
|
||||||
{ type: "range", min: 10, max: 2000, defaultValue: 800, property: "boxSize" },
|
|
||||||
{ type: "range", min: 1, max: 80, defaultValue: 5, property: "trailLength" },
|
|
||||||
{ type: "range", min: 1, max: 500, defaultValue: 5, property: "lineWidth" },
|
|
||||||
{ type: "checkbox", defaultValue: false, property: "fade" },
|
|
||||||
{ type: "color", defaultValue: "#43dbad", property: "colourFree" },
|
|
||||||
{ type: "color", defaultValue: "#f05c79", property: "colourContained" },
|
|
||||||
{ type: "header", text: "--CollisionBox---" },
|
|
||||||
{ type: "checkbox", defaultValue: false, property: "boxVisible" },
|
|
||||||
// {
|
|
||||||
// type: "dropdown",
|
|
||||||
// property: "exampleDropdown",
|
|
||||||
// defaultValue: "",
|
|
||||||
// options: [
|
|
||||||
// { value: "", label: "None" },
|
|
||||||
// { value: "field_white", label: "Field Whtie" },
|
|
||||||
// ]
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: "button",
|
|
||||||
// label: "Apply Background",
|
|
||||||
// method: "applyBackground"
|
|
||||||
// }
|
|
||||||
// {
|
|
||||||
// type: "range",
|
|
||||||
// min: 1,
|
|
||||||
// max: 10,
|
|
||||||
// defaultValue: 5,
|
|
||||||
// property: "magnitude",
|
|
||||||
// callback: (instance, newValue) => instance.setMagnitude(newValue)
|
|
||||||
// },
|
|
||||||
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return config[className];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function addControl(item, instance) {
|
|
||||||
let parentDiv = document.getElementById("shape-controls");
|
|
||||||
|
|
||||||
let title = document.createElement("p");
|
|
||||||
title.innerText = item.property + ": " + item.defaultValue;
|
|
||||||
title.id = "elText" + item.property;
|
|
||||||
|
|
||||||
let control;
|
|
||||||
let eventListener = null;
|
|
||||||
|
|
||||||
if (item.type === "range") {
|
|
||||||
control = document.createElement("input");
|
|
||||||
control.type = "range";
|
|
||||||
control.id = "el" + item.property;
|
|
||||||
control.min = item.min;
|
|
||||||
control.max = item.max;
|
|
||||||
control.value = item.defaultValue;
|
|
||||||
|
|
||||||
eventListener = (event) => {
|
|
||||||
const newValue = parseInt(event.target.value, 10);
|
|
||||||
instance[item.property] = newValue;
|
|
||||||
title.innerText = item.property + ": " + newValue;
|
|
||||||
|
|
||||||
if (item.callback) {
|
|
||||||
item.callback(instance, newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
control.addEventListener("input", eventListener);
|
|
||||||
|
|
||||||
} else if (item.type === "button") {
|
|
||||||
control = document.createElement("button");
|
|
||||||
control.innerText = item.label;
|
|
||||||
control.addEventListener("click", () => {
|
|
||||||
instance[item.method]();
|
|
||||||
});
|
|
||||||
} else if (item.type === "dropdown") {
|
|
||||||
control = document.createElement("select");
|
|
||||||
item.options.forEach(option => {
|
|
||||||
let optionElement = document.createElement("option");
|
|
||||||
optionElement.value = option.value;
|
|
||||||
optionElement.innerText = option.label;
|
|
||||||
control.appendChild(optionElement);
|
|
||||||
});
|
|
||||||
control.value = item.defaultValue;
|
|
||||||
control.addEventListener("change", (event) => {
|
|
||||||
const newValue = event.target.value;
|
|
||||||
instance[item.property] = newValue;
|
|
||||||
title.innerText = item.property + ": " + newValue;
|
|
||||||
|
|
||||||
if (item.callback) {
|
|
||||||
item.callback(instance, newValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (item.type === "header") {
|
|
||||||
control = document.createElement("p");
|
|
||||||
control.innerText = item.text;
|
|
||||||
control.className = "header";
|
|
||||||
control.id = "elHeader" + item.text.replace(/\s+/g, '');
|
|
||||||
// Headers are handled differently - add directly to parent
|
|
||||||
parentDiv.appendChild(control);
|
|
||||||
return { element: control, listener: eventListener };
|
|
||||||
}
|
|
||||||
else if (item.type === "color") {
|
|
||||||
control = document.createElement("input");
|
|
||||||
control.type = item.type;
|
|
||||||
control.value = item.defaultValue;
|
|
||||||
control.id = "el" + item.property;
|
|
||||||
|
|
||||||
eventListener = (event) => {
|
|
||||||
const newValue = event.target.value;
|
|
||||||
instance[item.property] = newValue;
|
|
||||||
title.innerText = item.property + ": " + newValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
control.addEventListener("input", eventListener);
|
|
||||||
}
|
|
||||||
else if (item.type === "checkbox") {
|
|
||||||
control = document.createElement("input");
|
|
||||||
control.type = item.type;
|
|
||||||
control.checked = item.defaultValue;
|
|
||||||
instance[item.property] = item.defaultValue;
|
|
||||||
control.id = "el" + item.property;
|
|
||||||
control.addEventListener("change", (event) => {
|
|
||||||
const newValue = event.target.checked;
|
|
||||||
instance[item.property] = newValue;
|
|
||||||
title.innerText = item.property + ": " + newValue;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.type != "header") {
|
|
||||||
control.className = "control";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create container div for the control
|
|
||||||
let containerDiv = document.createElement("div");
|
|
||||||
containerDiv.className = "control-container";
|
|
||||||
|
|
||||||
// Add title and control to container
|
|
||||||
if (item.type != "button") {
|
|
||||||
containerDiv.appendChild(title);
|
|
||||||
}
|
|
||||||
containerDiv.appendChild(control);
|
|
||||||
|
|
||||||
|
|
||||||
let filtersDiv = document.createElement("div");
|
|
||||||
|
|
||||||
// if (item.type === "range") {
|
|
||||||
// let addFilterButton = document.createElement("button");
|
|
||||||
// addFilterButton.innerText = "Add Filter";
|
|
||||||
// addFilterButton.className = "add-filter-button";
|
|
||||||
// addFilterButton.addEventListener("click", () => {
|
|
||||||
// const filterDiv = createFilter(item);
|
|
||||||
// filtersDiv.appendChild(filterDiv);
|
|
||||||
|
|
||||||
// });
|
|
||||||
// filtersDiv.appendChild(addFilterButton);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Add filters div at the bottom
|
|
||||||
filtersDiv.className = "control-filters";
|
|
||||||
filtersDiv.id = "filters-" + item.property;
|
|
||||||
containerDiv.appendChild(filtersDiv);
|
|
||||||
|
|
||||||
// Add the complete container to parent
|
|
||||||
parentDiv.appendChild(containerDiv);
|
|
||||||
|
|
||||||
return { element: control, listener: eventListener, filtersDiv: filtersDiv };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFilter(item) {
|
|
||||||
const filterDiv = document.createElement("div");
|
|
||||||
filterDiv.className = "filter-div";
|
|
||||||
filterDiv.innerText = "sin filter"; // Placeholder text
|
|
||||||
|
|
||||||
|
|
||||||
let minTitle = document.createElement("p");
|
|
||||||
minTitle.innerText = "Min:" + item.defaultValue;
|
|
||||||
filterDiv.appendChild(minTitle);
|
|
||||||
|
|
||||||
let sinMin = document.createElement("input");
|
|
||||||
sinMin.type = "range";
|
|
||||||
sinMin.id = "el-filter-" + item.property;
|
|
||||||
sinMin.min = -item.max;//item.min;
|
|
||||||
sinMin.max = item.max;
|
|
||||||
sinMin.value = item.defaultValue;
|
|
||||||
|
|
||||||
eventListener = (event) => {
|
|
||||||
const newValue = parseInt(event.target.value, 10);
|
|
||||||
// instance[item.property] = newValue;
|
|
||||||
minTitle.innerText = "Min: " + newValue;
|
|
||||||
|
|
||||||
if (item.callback) {
|
|
||||||
item.callback(instance, newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sinMin.addEventListener("input", eventListener);
|
|
||||||
filterDiv.appendChild(sinMin);
|
|
||||||
|
|
||||||
let maxTitle = document.createElement("p");
|
|
||||||
maxTitle.innerText = "Max:" + item.defaultValue;
|
|
||||||
filterDiv.appendChild(maxTitle);
|
|
||||||
|
|
||||||
|
|
||||||
let sinMax = document.createElement("input");
|
|
||||||
sinMax.type = "range";
|
|
||||||
sinMax.id = "el-filter-" + item.property;
|
|
||||||
sinMax.min = item.min;
|
|
||||||
sinMax.max = item.max;
|
|
||||||
sinMax.value = item.defaultValue;
|
|
||||||
|
|
||||||
eventListener = (event) => {
|
|
||||||
const newValue = parseInt(event.target.value, 10);
|
|
||||||
// instance[item.property] = newValue;
|
|
||||||
maxTitle.innerText = "Max: " + newValue;
|
|
||||||
|
|
||||||
if (item.callback) {
|
|
||||||
item.callback(instance, newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sinMax.addEventListener("input", eventListener);
|
|
||||||
filterDiv.appendChild(sinMax);
|
|
||||||
|
|
||||||
let rate = createFilterSlider("Rate", item, filterDiv);
|
|
||||||
|
|
||||||
return { filterDiv, min: sinMin, max: sinMax, rate: rate};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFilterSlider(name, item, filterDiv) {
|
|
||||||
let minTitle = document.createElement("p");
|
|
||||||
minTitle.innerText = name + ":" + item.defaultValue;
|
|
||||||
filterDiv.appendChild(minTitle);
|
|
||||||
|
|
||||||
let sinMin = document.createElement("input");
|
|
||||||
sinMin.type = "range";
|
|
||||||
sinMin.id = "el-filter-" + item.property;
|
|
||||||
sinMin.min = item.min;
|
|
||||||
sinMin.max = item.max;
|
|
||||||
sinMin.value = item.defaultValue;
|
|
||||||
|
|
||||||
eventListener = (event) => {
|
|
||||||
const newValue = parseInt(event.target.value, 10);
|
|
||||||
// instance[item.property] = newValue;
|
|
||||||
minTitle.innerText = name + ": " + newValue;
|
|
||||||
|
|
||||||
if (item.callback) {
|
|
||||||
item.callback(instance, newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sinMin.addEventListener("input", eventListener);
|
|
||||||
filterDiv.appendChild(sinMin);
|
|
||||||
return sinMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateControlInput(value, controlName) {// Find and update the slider element
|
|
||||||
const elementSlider = document.querySelector('input[type="range"][id="el' + controlName + '"]');
|
|
||||||
if (elementSlider) {
|
|
||||||
// Update the slider value
|
|
||||||
elementSlider.value = value;
|
|
||||||
|
|
||||||
// Update the text display
|
|
||||||
const elementSliderText = document.getElementById(`elText${controlName}`);
|
|
||||||
if (elementSliderText) {
|
|
||||||
elementSliderText.innerText = `${controlName}: ${Math.round(value)}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function drawEyelid(width, x1, y1, colour) {
|
|
||||||
x1 -= centerX;
|
|
||||||
y1 -= centerY;
|
|
||||||
|
|
||||||
const angle = Math.atan2(y1, x1);
|
|
||||||
const cosAngle = Math.cos(angle);
|
|
||||||
const sinAngle = Math.sin(angle);
|
|
||||||
|
|
||||||
const x2 = cosAngle * width;
|
|
||||||
const y2 = sinAngle * width;
|
|
||||||
|
|
||||||
const x3Old = width / 2;
|
|
||||||
const y3Old = width / 2;
|
|
||||||
const x4Old = width / 2;
|
|
||||||
const y4Old = -width / 2;
|
|
||||||
|
|
||||||
const x3 = x3Old * cosAngle - y3Old * sinAngle;
|
|
||||||
const y3 = x3Old * sinAngle + y3Old * cosAngle;
|
|
||||||
const x4 = x4Old * cosAngle - y4Old * sinAngle;
|
|
||||||
const y4 = x4Old * sinAngle + y4Old * cosAngle;
|
|
||||||
|
|
||||||
x1 += centerX;
|
|
||||||
y1 += centerY;
|
|
||||||
const x2Final = x2 + x1;
|
|
||||||
const y2Final = y2 + y1;
|
|
||||||
const x3Final = x3 + x1;
|
|
||||||
const y3Final = y3 + y1;
|
|
||||||
const x4Final = x4 + x1;
|
|
||||||
const y4Final = y4 + y1;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x3Final, y3Final, x2Final, y2Final);
|
|
||||||
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x4Final, y4Final, x2Final, y2Final);
|
|
||||||
ctx.fillStyle = colour;
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.strokeStyle = "black";
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawEyelidAccident(x1, y1) {
|
|
||||||
let leafWidth = 120;
|
|
||||||
let leafHeight = 60;
|
|
||||||
x1 -= centerX;
|
|
||||||
y1 -= centerY;
|
|
||||||
let angle = Math.atan(y1 / x1);
|
|
||||||
// if(angle >=Math.PI){
|
|
||||||
// angle -=Math.PI
|
|
||||||
// console.log("greater called")
|
|
||||||
// }
|
|
||||||
angle = Math.abs(angle);
|
|
||||||
let x2Old = 0 + leafWidth;
|
|
||||||
let y2Old = 0;
|
|
||||||
|
|
||||||
let x3Old = 0 + leafWidth / 2;
|
|
||||||
let y3Old = 0 + leafHeight / 2;
|
|
||||||
|
|
||||||
let x4Old = 0 + leafWidth / 2;
|
|
||||||
let y4Old = 0 - leafHeight / 2;
|
|
||||||
|
|
||||||
let x2 = x2Old * Math.cos(angle) - y2Old * Math.sin(angle);
|
|
||||||
let y2 = x2Old * Math.sin(angle) + y2Old * Math.cos(angle);
|
|
||||||
|
|
||||||
let x3 = x3Old * Math.cos(angle) - y3Old * Math.sin(angle);
|
|
||||||
let y3 = x3Old * Math.sin(angle) + y3Old * Math.cos(angle);
|
|
||||||
|
|
||||||
let x4 = x4Old * Math.cos(angle) - y4Old * Math.sin(angle);
|
|
||||||
let y4 = x4Old * Math.sin(angle) + y4Old * Math.cos(angle);
|
|
||||||
|
|
||||||
let oldx1 = x1;
|
|
||||||
let oldy1 = y1;
|
|
||||||
|
|
||||||
x1 += centerX; // +x2/2
|
|
||||||
y1 += centerY; // +x2/2
|
|
||||||
x2 += x1;
|
|
||||||
y2 += y1;
|
|
||||||
x3 += x1;
|
|
||||||
y3 += y1;
|
|
||||||
x4 += x1;
|
|
||||||
y4 += y1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x3, y3, x2, y2);
|
|
||||||
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x4, y4, x2, y2);
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
ctx.lineWidth = 1;
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x3, y3, x2, y2);
|
|
||||||
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.quadraticCurveTo(x4, y4, x2, y2);
|
|
||||||
ctx.strokeStyle = "orange";
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
function DrawPolygon(sides, width, rotation, colour, line_width) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(
|
|
||||||
centerX + width * Math.cos((rotation * Math.PI) / 180),
|
|
||||||
centerY + width * Math.sin((rotation * Math.PI) / 180)
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var i = 1; i <= sides; i += 1) {
|
|
||||||
ctx.lineTo(
|
|
||||||
centerX +
|
|
||||||
width *
|
|
||||||
Math.cos((i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180),
|
|
||||||
centerY +
|
|
||||||
width * Math.sin((i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ctx.strokeStyle = colour;
|
|
||||||
ctx.lineWidth = line_width;
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
function rad(degrees) {
|
|
||||||
return (degrees * Math.PI) / 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
function colourToText(colour) {
|
|
||||||
return "rgb(" + colour[0] + "," + colour[1] + "," + colour[2] + ")";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function waveNormal(x, max) {
|
|
||||||
let val = Math.sin((x / max) * Math.PI * 2 - max * (Math.PI / (max * 2))) / 2 + 0.5
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
|
|
||||||
function hexToRgb(hex) {
|
|
||||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
||||||
return result ? {
|
|
||||||
r: parseInt(result[1], 16),
|
|
||||||
g: parseInt(result[2], 16),
|
|
||||||
b: parseInt(result[3], 16)
|
|
||||||
} : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LerpHex(a, b, amount) {
|
|
||||||
var ah = parseInt(a.replace(/#/g, ""), 16),
|
|
||||||
ar = ah >> 16,
|
|
||||||
ag = (ah >> 8) & 0xff,
|
|
||||||
ab = ah & 0xff,
|
|
||||||
bh = parseInt(b.replace(/#/g, ""), 16),
|
|
||||||
br = bh >> 16,
|
|
||||||
bg = (bh >> 8) & 0xff,
|
|
||||||
bb = bh & 0xff,
|
|
||||||
rr = ar + amount * (br - ar),
|
|
||||||
rg = ag + amount * (bg - ag),
|
|
||||||
rb = ab + amount * (bb - ab);
|
|
||||||
|
|
||||||
return (
|
|
||||||
"#" + (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LerpRGB(a, b, t) {
|
|
||||||
if (t < 0) {
|
|
||||||
t *= -1;
|
|
||||||
}
|
|
||||||
var newColor = [0, 0, 0];
|
|
||||||
newColor[0] = a[0] + (b[0] - a[0]) * t;
|
|
||||||
newColor[1] = a[1] + (b[1] - a[1]) * t;
|
|
||||||
newColor[2] = a[2] + (b[2] - a[2]) * t;
|
|
||||||
return newColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lerpRGB(a, b, t) {
|
|
||||||
const result = [0, 0, 0];
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
result[i] = (1 - t) * a[i] + t * b[i];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function drawCenter(width) {
|
|
||||||
console.log("center?")
|
|
||||||
ctx.strokeStyle = "pink";
|
|
||||||
ctx.lineWidth = 1
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(centerX - width, centerY);
|
|
||||||
ctx.lineTo(centerX + width, centerY);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(centerX, centerY - width);
|
|
||||||
ctx.lineTo(centerX, centerY + width);
|
|
||||||
ctx.closePath();
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
|
|
||||||
function render_clear() {
|
|
||||||
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotatePointTmp(x, y, centerXX, centerYY, rotation) {
|
|
||||||
let xFromC = x - centerXX;
|
|
||||||
let yFromC = y - centerYY;
|
|
||||||
let d = (xFromC ** 2 + yFromC ** 2) ** 0.5
|
|
||||||
// let orgAngle = Math.atan2(yFromC/xFromC)
|
|
||||||
let orgAngle = Math.atan2(xFromC, yFromC)
|
|
||||||
let tmp = Math.cos(rad(orgAngle - rotation)) * d
|
|
||||||
// console.log(Math.cos((-90)*(Math.PI/180)))
|
|
||||||
console.log(orgAngle)
|
|
||||||
console.log(rad(rotation))
|
|
||||||
console.log(Math.cos(orgAngle - rad(rotation)) * d)
|
|
||||||
console.log(d)
|
|
||||||
// console.log(d)
|
|
||||||
let newPointX = Math.cos(orgAngle - rad(rotation + 90)) * d + centerXX;
|
|
||||||
let newPointY = Math.sin(orgAngle - rad(rotation + 90)) * d + centerYY;
|
|
||||||
return [newPointX, newPointY]
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotatePoint(x, y, rotation) {
|
|
||||||
let nCos = Math.cos(rad(rotation))
|
|
||||||
let nSin = Math.sin(rad(rotation))
|
|
||||||
let newX = x * nCos - y * nSin
|
|
||||||
let newY = y * nCos + x * nSin
|
|
||||||
return [newX, newY]
|
|
||||||
}
|
|
||||||
173
docs/js/index.js
173
docs/js/index.js
@@ -1,128 +1,46 @@
|
|||||||
//jshint esversion:8
|
//jshint esversion:8
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Animation Framework - Main Entry Point
|
||||||
|
*
|
||||||
|
* This file initializes the animation engine and control system.
|
||||||
|
* Shapes are registered via shapeRegistry and managed through the Scene.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Global references (for backward compatibility with shapes)
|
||||||
|
// ============================================
|
||||||
let c = document.getElementById("myCanvas");
|
let c = document.getElementById("myCanvas");
|
||||||
let ctx = c.getContext("2d");
|
let ctx = c.getContext("2d");
|
||||||
ctx.canvas.width = window.innerWidth;
|
|
||||||
ctx.canvas.height = window.innerHeight;
|
|
||||||
let centerX = ctx.canvas.width / 2;
|
let centerX = ctx.canvas.width / 2;
|
||||||
let centerY = ctx.canvas.height / 2;
|
let centerY = ctx.canvas.height / 2;
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// Initialize Core Systems
|
||||||
|
// ============================================
|
||||||
|
|
||||||
let deg_per_sec = 10;
|
// Create animation engine and connect it to the scene
|
||||||
let targetFps = 60;
|
const engine = new AnimationEngine('myCanvas');
|
||||||
let frameDuration = 1000 / targetFps;
|
engine.setScene(scene);
|
||||||
|
|
||||||
let rotation = 0; //was = j = angle
|
// Initialize Scene UI
|
||||||
let paused = false;
|
const sceneUI = new SceneUI(scene, 'layers-container');
|
||||||
let elapsedTime = 0;
|
|
||||||
let lastTimestamp = 0;
|
|
||||||
render_clear();
|
|
||||||
|
|
||||||
let drawObj = null;
|
// ============================================
|
||||||
function createInstance(className, args) {
|
// Event Listeners
|
||||||
const classMap = {
|
// ============================================
|
||||||
NewWave: NewWave,
|
|
||||||
Countdown: Countdown,
|
|
||||||
RaysInShape: RaysInShape,
|
|
||||||
PolyTwistColourWidth: PolyTwistColourWidth,
|
|
||||||
FloralPhyllo: FloralPhyllo,
|
|
||||||
Spiral1: Spiral1,
|
|
||||||
FloralAccident: FloralAccident,
|
|
||||||
FloralPhyllo_Accident: FloralPhyllo_Accident,
|
|
||||||
Nodal_expanding: Nodal_expanding,
|
|
||||||
Phyllotaxis: Phyllotaxis,
|
|
||||||
SquareTwist_angle: SquareTwist_angle,
|
|
||||||
EyePrototype: EyePrototype,
|
|
||||||
CircleExpand: CircleExpand,
|
|
||||||
MaryFace: MaryFace,
|
|
||||||
// Add more class constructors here as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
if (classMap.hasOwnProperty(className)) {
|
|
||||||
return new classMap[className](...args);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unknown class name: ${className}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function updateDrawObj() {
|
|
||||||
const shapeSelector = document.getElementById("shape-selector");
|
|
||||||
const selectedShape = shapeSelector.value;
|
|
||||||
const config = await fetchConfig(selectedShape);
|
|
||||||
if (drawObj) {
|
|
||||||
drawObj.remove(); // Remove the previous instance
|
|
||||||
}
|
|
||||||
// Extract default values from the configuration
|
|
||||||
const defaultValues = config
|
|
||||||
// .filter((item) => item.type !== "color") // Exclude color inputs
|
|
||||||
.map((item) => item.defaultValue);
|
|
||||||
|
|
||||||
drawObj = createInstance(selectedShape, defaultValues);
|
|
||||||
|
|
||||||
// drawObj = await createShapeWithRandomProperties(813311281, config1);
|
|
||||||
console.log(drawObj)
|
|
||||||
drawObj.initialise(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDrawObj();
|
|
||||||
|
|
||||||
function render(timestamp) {
|
|
||||||
if (!lastTimestamp) lastTimestamp = timestamp;
|
|
||||||
const deltaTime = timestamp - lastTimestamp;
|
|
||||||
const adjustedElapsed = elapsedTime / 100; // Convert to seconds
|
|
||||||
lastTimestamp = timestamp;
|
|
||||||
let adjustedDeltaTime;
|
|
||||||
if (!paused) {
|
|
||||||
rotation += deg_per_sec / targetFps;
|
|
||||||
elapsedTime += deltaTime;
|
|
||||||
adjustedDeltaTime = deltaTime / 100; // Convert to seconds
|
|
||||||
// console.log(adjustedDeltaTime)
|
|
||||||
}
|
|
||||||
// console.log(deltaTime)
|
|
||||||
// console.log(elapsedTime)
|
|
||||||
render_clear();
|
|
||||||
if (drawObj) {
|
|
||||||
// drawObj.draw(rotation);
|
|
||||||
|
|
||||||
drawObj.draw(adjustedElapsed, adjustedDeltaTime);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ctx.font = "48px serif";
|
|
||||||
// ctx.fillStyle = "white"
|
|
||||||
// ctx.fillText(Math.floor(elapsedTime) + "ms", centerX - 100, centerY + 400);
|
|
||||||
// drawCenter(300)
|
|
||||||
|
|
||||||
requestAnimationFrame(render);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
document
|
|
||||||
.getElementById("shape-selector")
|
|
||||||
.addEventListener("change", updateDrawObj);
|
|
||||||
|
|
||||||
let toolbarShowing = true;
|
let toolbarShowing = true;
|
||||||
document.addEventListener("keydown", toggleSettings);
|
document.addEventListener("keydown", toggleSettings);
|
||||||
|
|
||||||
// Add resize event listener
|
// ============================================
|
||||||
window.addEventListener('resize', function () {
|
// UI Control Functions
|
||||||
ctx.canvas.width = window.innerWidth;
|
// ============================================
|
||||||
ctx.canvas.height = window.innerHeight;
|
|
||||||
centerX = ctx.canvas.width / 2;
|
|
||||||
centerY = ctx.canvas.height / 2;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
function manualToggleSettings() {
|
function manualToggleSettings() {
|
||||||
console.log("hi")
|
|
||||||
toolbarShowing = !toolbarShowing;
|
toolbarShowing = !toolbarShowing;
|
||||||
let tb = document.getElementById("toolbar");
|
let tb = document.getElementById("toolbar");
|
||||||
if (toolbarShowing) {
|
tb.style.display = toolbarShowing ? "flex" : "none";
|
||||||
tb.style.display = "flex";
|
|
||||||
} else {
|
|
||||||
tb.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleSettings(e) {
|
function toggleSettings(e) {
|
||||||
@@ -130,43 +48,40 @@ function toggleSettings(e) {
|
|||||||
toolbarShowing = !toolbarShowing;
|
toolbarShowing = !toolbarShowing;
|
||||||
}
|
}
|
||||||
if (e.code === "Space") {
|
if (e.code === "Space") {
|
||||||
paused = !paused;
|
engine.togglePause();
|
||||||
}
|
}
|
||||||
|
|
||||||
let tb = document.getElementById("toolbar");
|
let tb = document.getElementById("toolbar");
|
||||||
if (toolbarShowing) {
|
tb.style.display = toolbarShowing ? "flex" : "none";
|
||||||
tb.style.display = "flex";
|
|
||||||
} else {
|
|
||||||
tb.style.display = "none";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TogglePause() {
|
function TogglePause() {
|
||||||
let pb = document.getElementById("pauseButton");
|
let pb = document.getElementById("pauseButton");
|
||||||
paused = !paused;
|
const paused = engine.togglePause();
|
||||||
|
pb.textContent = paused ? "Play" : "Pause";
|
||||||
if (paused) {
|
|
||||||
pb.textContent = "Play";
|
|
||||||
} else {
|
|
||||||
pb.textContent = "Pause";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Reset() {
|
function Reset() {
|
||||||
rotation = 0; //was = j = angle
|
engine.reset();
|
||||||
currentFrame = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ForwardFrame() {
|
function ForwardFrame() {
|
||||||
rotation += deg_per_sec / targetFps; // was = j = innerRotation, now = rotation
|
engine.stepForward();
|
||||||
currentFrame += 1; // was = i
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function BackwardFrame() {
|
function BackwardFrame() {
|
||||||
rotation -= deg_per_sec / targetFps; // was = j = innerRotation, now = rotation
|
engine.stepBackward();
|
||||||
currentFrame -= 1; // was = i
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChangeDegPerSec(newValue) {
|
function ChangeDegPerSec(newValue) {
|
||||||
deg_per_sec = newValue;
|
engine.setSpeed(newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onload = requestAnimationFrame(render);
|
function render_clear() {
|
||||||
|
engine.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start animation
|
||||||
|
window.onload = function() {
|
||||||
|
engine.start();
|
||||||
|
};
|
||||||
|
|||||||
1211
docs/js/objects.js
1211
docs/js/objects.js
File diff suppressed because it is too large
Load Diff
198
docs/js/presets.js
Normal file
198
docs/js/presets.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
/**
|
||||||
|
* Built-in Presets
|
||||||
|
*
|
||||||
|
* Add your favorite scene configurations here!
|
||||||
|
* Each preset has: name, description, and data (scene export)
|
||||||
|
*/
|
||||||
|
const BUILT_IN_PRESETS = [
|
||||||
|
{
|
||||||
|
name: "Hypnotic Spiral",
|
||||||
|
description: "Classic spiral pattern with smooth colors",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "Spiral1",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: {
|
||||||
|
width: 400,
|
||||||
|
n: 6,
|
||||||
|
piv: 60,
|
||||||
|
step: 1,
|
||||||
|
line_width: 2
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Floral Bloom",
|
||||||
|
description: "Organic flower-like pattern",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "FloralPhyllo",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: {
|
||||||
|
width: 500,
|
||||||
|
n: 8,
|
||||||
|
piv: 45,
|
||||||
|
start: 0,
|
||||||
|
end: 180,
|
||||||
|
step: 1,
|
||||||
|
line_width: 1
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Poly Twist Rainbow",
|
||||||
|
description: "Colorful twisting polygon",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "PolyTwistColourWidth",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: {
|
||||||
|
sides: 6,
|
||||||
|
width: 500,
|
||||||
|
line_width: 2,
|
||||||
|
depth: 80,
|
||||||
|
rotation: 0,
|
||||||
|
colour1: "#ff0066",
|
||||||
|
colour2: "#00ffff"
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Expanding Nodes",
|
||||||
|
description: "Pulsing nodal pattern",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "Nodal_expanding",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: {
|
||||||
|
width: 300,
|
||||||
|
nodeCount: 12,
|
||||||
|
start: 0,
|
||||||
|
colour1: "#ffffff",
|
||||||
|
colour2: "#4a9eff"
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Wave Machine",
|
||||||
|
description: "Mesmerizing sine wave radiation",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "NewWave",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: false,
|
||||||
|
values: {
|
||||||
|
width: 967,
|
||||||
|
sides: 8,
|
||||||
|
step: 42,
|
||||||
|
lineWidth: 3,
|
||||||
|
limiter: 159,
|
||||||
|
speedMultiplier: 556
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
limiter: [
|
||||||
|
{
|
||||||
|
type: "sin",
|
||||||
|
params: {
|
||||||
|
min: 42,
|
||||||
|
max: 240,
|
||||||
|
rate: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
,
|
||||||
|
controlBounds: {
|
||||||
|
limiter: {
|
||||||
|
min: 1,
|
||||||
|
max: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Black Hole Spiral",
|
||||||
|
description: "Two intertwined spirals",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "Spiral1",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: { width: 350, n: 4, piv: 90, step: 1, line_width: 2,
|
||||||
|
colour: "#7734D3"
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Phyllotaxis Garden",
|
||||||
|
description: "Nature-inspired spiral arrangement",
|
||||||
|
builtIn: true,
|
||||||
|
data: {
|
||||||
|
version: 1,
|
||||||
|
layers: [
|
||||||
|
{
|
||||||
|
name: "Phyllotaxis",
|
||||||
|
visible: true,
|
||||||
|
paused: false,
|
||||||
|
collapsed: true,
|
||||||
|
values: {
|
||||||
|
n: 300,
|
||||||
|
c: 8,
|
||||||
|
start: 137.5,
|
||||||
|
colour1: "#ff6b6b",
|
||||||
|
colour2: "#4ecdc4",
|
||||||
|
dotSize: 6,
|
||||||
|
mode: "spiral"
|
||||||
|
},
|
||||||
|
filters: {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
99
docs/js/shapes/CircleExpand.js
Normal file
99
docs/js/shapes/CircleExpand.js
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* CircleExpand - Expanding concentric circles/hearts animation
|
||||||
|
*/
|
||||||
|
class CircleExpand extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 70, defaultValue: 21, property: 'nCircles' },
|
||||||
|
{ type: 'range', min: 50, max: 150, defaultValue: 150, property: 'gap' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'linear' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'heart' },
|
||||||
|
{ type: 'color', defaultValue: '#fc03cf', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#00fffb', property: 'colour2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(nCircles, gap, linear, heart, colour1, colour2) {
|
||||||
|
super();
|
||||||
|
this.nCircles = nCircles;
|
||||||
|
this.gap = gap;
|
||||||
|
this.linear = linear;
|
||||||
|
this.heart = heart;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
}
|
||||||
|
|
||||||
|
lerpColor(a, b, amount) {
|
||||||
|
const ah = +a.replace('#', '0x');
|
||||||
|
const ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff;
|
||||||
|
const bh = +b.replace('#', '0x');
|
||||||
|
const br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff;
|
||||||
|
const rr = ar + amount * (br - ar);
|
||||||
|
const rg = ag + amount * (bg - ag);
|
||||||
|
const rb = ab + amount * (bb - ab);
|
||||||
|
return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
arraySort(x, y) {
|
||||||
|
if (x.r > y.r) return 1;
|
||||||
|
if (x.r < y.r) return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawHeart(w, colour) {
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.fillStyle = colour;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
const x = centerX - w / 2;
|
||||||
|
const y = centerY - w / 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y + w / 4);
|
||||||
|
ctx.quadraticCurveTo(x, y, x + w / 4, y);
|
||||||
|
ctx.quadraticCurveTo(x + w / 2, y, x + w / 2, y + w / 5);
|
||||||
|
ctx.quadraticCurveTo(x + w / 2, y, x + w * 3 / 4, y);
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + w / 4);
|
||||||
|
ctx.quadraticCurveTo(x + w, y + w / 2, x + w * 3 / 4, y + w * 3 / 4);
|
||||||
|
ctx.lineTo(x + w / 2, y + w);
|
||||||
|
ctx.lineTo(x + w / 4, y + w * 3 / 4);
|
||||||
|
ctx.quadraticCurveTo(x, y + w / 2, x, y + w / 4);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
const rotation = elapsed * 0.9;
|
||||||
|
ctx.strokeWeight = 1;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
const arrOfWidths = [];
|
||||||
|
let intRot;
|
||||||
|
|
||||||
|
if (this.linear) {
|
||||||
|
intRot = Math.floor(rotation * 30) / 100;
|
||||||
|
} else {
|
||||||
|
intRot = Math.sin(rad(Math.floor(rotation * 30) / 4)) + rotation / 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < this.nCircles; i++) {
|
||||||
|
const width = this.gap * ((intRot + i) % this.nCircles);
|
||||||
|
const colour = (Math.sin(rad(i * (360 / this.nCircles) - 90)) + 1) / 2;
|
||||||
|
arrOfWidths.push({ r: width, c: colour });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newArr = arrOfWidths.sort(this.arraySort);
|
||||||
|
|
||||||
|
for (let i = this.nCircles - 1; i >= 0; i--) {
|
||||||
|
const newColour = this.lerpColor(this.colour1, this.colour2, newArr[i].c);
|
||||||
|
|
||||||
|
if (this.heart) {
|
||||||
|
this.drawHeart(newArr[i].r, newColour);
|
||||||
|
} else {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(centerX, centerY, newArr[i].r, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = newColour;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('CircleExpand', CircleExpand);
|
||||||
78
docs/js/shapes/Countdown.js
Normal file
78
docs/js/shapes/Countdown.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Countdown - Countdown timer display with progress bar
|
||||||
|
*/
|
||||||
|
class Countdown extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 8000, max: 2000000, defaultValue: 2000000, property: 'milestone' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(milestone) {
|
||||||
|
super();
|
||||||
|
this.milestone = milestone;
|
||||||
|
}
|
||||||
|
|
||||||
|
secondsUntilDate(targetDate) {
|
||||||
|
const now = new Date();
|
||||||
|
const nzTimeString = targetDate.replace('T', 'T').concat('+12:00');
|
||||||
|
const target = new Date(nzTimeString);
|
||||||
|
const difference = target.getTime() - now.getTime();
|
||||||
|
return Math.round(difference / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawProgressBar(progress) {
|
||||||
|
const colourBackground = "#0c2f69";
|
||||||
|
const colourProgress = "#4287f5";
|
||||||
|
const barWidth = ctx.canvas.width;
|
||||||
|
const barHeight = 60;
|
||||||
|
const barX = 0;
|
||||||
|
const barY = ctx.canvas.height - barHeight;
|
||||||
|
|
||||||
|
ctx.fillStyle = colourBackground;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(barX, barY, barWidth, barHeight);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.fillStyle = colourProgress;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.rect(barX, barY, (barWidth / 100) * progress, barHeight);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
let fontSize = 48;
|
||||||
|
if (ctx.canvas.width < 1000) {
|
||||||
|
fontSize = 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.font = fontSize + "px serif";
|
||||||
|
ctx.fillStyle = "white";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
|
||||||
|
const futureDate = '2025-06-01T04:30:00';
|
||||||
|
const seconds = this.secondsUntilDate(futureDate);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
const percentRounded = (((elapsed / 1000) / seconds) * 100).toFixed(8);
|
||||||
|
|
||||||
|
ctx.fillText(seconds + " Seconds", centerX, centerY - 200);
|
||||||
|
ctx.fillText(minutes + " Minutes", centerX, centerY - 100);
|
||||||
|
ctx.fillText(hours + " Hours", centerX, centerY);
|
||||||
|
ctx.fillText(percentRounded + "% Closer", centerX, centerY + 300);
|
||||||
|
|
||||||
|
const milestoneSeconds = this.milestone;
|
||||||
|
const target = new Date(futureDate + '+12:00');
|
||||||
|
const milestoneDate = new Date(target.getTime() - milestoneSeconds * 1000).toLocaleString();
|
||||||
|
ctx.fillText(milestoneDate, centerX, centerY + 100);
|
||||||
|
ctx.fillText("^-- " + milestoneSeconds + " milestone", centerX, centerY + 200);
|
||||||
|
|
||||||
|
const canvasWidth = ctx.canvas.width;
|
||||||
|
const secondsPerPixel = (seconds / canvasWidth);
|
||||||
|
const secondsUntilFirstPixel = secondsPerPixel - (elapsed / 10);
|
||||||
|
|
||||||
|
ctx.fillText("Time until first pixel: " + Math.round(secondsUntilFirstPixel) + " seconds", centerX, centerY + 350);
|
||||||
|
|
||||||
|
this.drawProgressBar(percentRounded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('Countdown', Countdown);
|
||||||
196
docs/js/shapes/EyePrototype.js
Normal file
196
docs/js/shapes/EyePrototype.js
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* EyePrototype - Animated eye with blinking, spiral, and hypnotic effects
|
||||||
|
*/
|
||||||
|
class EyePrototype extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: 0, property: 'x' },
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: 0, property: 'y' },
|
||||||
|
{ type: 'range', min: -180, max: 180, defaultValue: 0, property: 'rotate' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'flip' },
|
||||||
|
{ type: 'range', min: 1, max: 800, defaultValue: 400, property: 'width' },
|
||||||
|
{ type: 'range', min: 1, max: 100, defaultValue: 5, property: 'blink_speed' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_spiral' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'spiral_full' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_pupil' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_expand' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'draw_hypno' },
|
||||||
|
{ type: 'range', min: 1, max: 10, defaultValue: 1, property: 'line_width' },
|
||||||
|
{ type: 'color', defaultValue: '#00fffb', property: 'colourPupil' },
|
||||||
|
{ type: 'color', defaultValue: '#ff0000', property: 'colourSpiral' },
|
||||||
|
{ type: 'color', defaultValue: '#00fffb', property: 'colourExpand' },
|
||||||
|
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'draw_eyelid' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(x, y, rotate, flip, width, blink_speed, draw_spiral, spiral_full, draw_pupil, draw_expand, draw_hypno, line_width, colourPupil, colourSpiral, colourExpand) {
|
||||||
|
super();
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.rotate = rotate;
|
||||||
|
this.flip = flip;
|
||||||
|
this.width = width;
|
||||||
|
this.blink_speed = blink_speed;
|
||||||
|
this.line_width = line_width;
|
||||||
|
this.step = 0;
|
||||||
|
this.opening = true;
|
||||||
|
this.counter = 0;
|
||||||
|
this.cooldown = 0;
|
||||||
|
this.draw_spiral = draw_spiral;
|
||||||
|
this.spiral_full = spiral_full;
|
||||||
|
this.draw_pupil = draw_pupil;
|
||||||
|
this.draw_expand = draw_expand;
|
||||||
|
this.draw_hypno = draw_hypno;
|
||||||
|
this.colourPupil = colourPupil;
|
||||||
|
this.colourSpiral = colourSpiral;
|
||||||
|
this.colourExpand = colourExpand;
|
||||||
|
this.centerPulse = new CircleExpand(10, 30, 1, 0, "#2D81FC", "#FC0362");
|
||||||
|
}
|
||||||
|
|
||||||
|
drawEyelid(rotation) {
|
||||||
|
ctx.strokeStyle = "orange";
|
||||||
|
const relCenterX = centerX + this.x;
|
||||||
|
const relCenterY = centerY + this.y;
|
||||||
|
rotation *= (this.speedMultiplier / 100);
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
let newPoint = 0;
|
||||||
|
let newPoint1 = 0;
|
||||||
|
const addedRotate = this.flip ? 90 : 0;
|
||||||
|
|
||||||
|
newPoint = rotatePoint(-this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
ctx.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
|
||||||
|
newPoint = rotatePoint(0, -rotation / 400 * this.width, this.rotate + addedRotate);
|
||||||
|
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
ctx.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
|
||||||
|
|
||||||
|
newPoint = rotatePoint(-this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
ctx.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
|
||||||
|
newPoint = rotatePoint(0, +rotation / 400 * this.width, this.rotate + addedRotate);
|
||||||
|
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
ctx.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
eyelidCut(rotation) {
|
||||||
|
const relCenterX = centerX + this.x;
|
||||||
|
const relCenterY = centerY + this.y;
|
||||||
|
let newPoint = 0;
|
||||||
|
let newPoint1 = 0;
|
||||||
|
const addedRotate = this.flip ? 90 : 0;
|
||||||
|
|
||||||
|
const squarePath = new Path2D();
|
||||||
|
newPoint = rotatePoint(-this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
squarePath.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
|
||||||
|
newPoint = rotatePoint(0, -rotation / 400 * this.width, this.rotate + addedRotate);
|
||||||
|
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
squarePath.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
|
||||||
|
|
||||||
|
newPoint = rotatePoint(-this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
squarePath.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
|
||||||
|
newPoint = rotatePoint(0, +rotation / 400 * this.width, this.rotate + addedRotate);
|
||||||
|
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate);
|
||||||
|
squarePath.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
|
||||||
|
|
||||||
|
ctx.clip(squarePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawGrowEye(step) {
|
||||||
|
ctx.strokeStyle = this.colourExpand;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.arc(centerX + this.x, centerY + this.y, step, 0, 2 * Math.PI);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawCircle(step) {
|
||||||
|
ctx.strokeStyle = this.colourPupil;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
ctx.arc(centerX + this.x, centerY + this.y, step, 0, 2 * Math.PI);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSpiral(step) {
|
||||||
|
ctx.strokeStyle = this.colourSpiral;
|
||||||
|
const a = 1;
|
||||||
|
const b = 5;
|
||||||
|
ctx.moveTo(centerX, centerY);
|
||||||
|
ctx.beginPath();
|
||||||
|
const max = this.spiral_full ? this.width : this.width / 2;
|
||||||
|
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
const angle = 0.1 * i;
|
||||||
|
const x = centerX + (a + b * angle) * Math.cos(angle + step / 2);
|
||||||
|
const y = centerY + (a + b * angle) * Math.sin(angle + step / 2);
|
||||||
|
ctx.lineTo(x + this.x, y + this.y);
|
||||||
|
}
|
||||||
|
ctx.lineWidth = 3;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
stepFunc() {
|
||||||
|
if (this.cooldown !== 0) {
|
||||||
|
this.cooldown--;
|
||||||
|
} else {
|
||||||
|
if (this.opening === true) {
|
||||||
|
if (this.step >= 200) {
|
||||||
|
this.cooldown = 200;
|
||||||
|
this.opening = false;
|
||||||
|
this.step -= this.blink_speed;
|
||||||
|
} else {
|
||||||
|
this.step += this.blink_speed;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.step <= 0) {
|
||||||
|
this.opening = true;
|
||||||
|
this.step += this.blink_speed;
|
||||||
|
} else {
|
||||||
|
this.step -= this.blink_speed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
const speedMult = 50;
|
||||||
|
const waitTime = this.blink_speed;
|
||||||
|
const cap = 200;
|
||||||
|
const d = waitTime * speedMult * 10;
|
||||||
|
const a = cap * 2 + d;
|
||||||
|
const outputRotation = Math.min(Math.abs((Math.floor(elapsed * speedMult) % a) - a / 2 - d / 2), cap);
|
||||||
|
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.save();
|
||||||
|
this.drawEyelid(outputRotation);
|
||||||
|
this.eyelidCut(outputRotation);
|
||||||
|
|
||||||
|
if (Math.floor(this.counter % (this.width / 4)) === 0) {
|
||||||
|
this.counter = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fillRect(this.x - this.width / 2 + centerX, 0, this.width, ctx.canvas.height);
|
||||||
|
|
||||||
|
if (this.draw_expand) {
|
||||||
|
this.drawGrowEye(this.width / 4 + this.counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.draw_hypno) {
|
||||||
|
this.centerPulse.draw(elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.draw_spiral) {
|
||||||
|
this.drawSpiral(elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.draw_pupil) {
|
||||||
|
this.drawCircle(this.width / 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
this.stepFunc();
|
||||||
|
this.counter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('EyePrototype', EyePrototype);
|
||||||
59
docs/js/shapes/FloralAccident.js
Normal file
59
docs/js/shapes/FloralAccident.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* FloralAccident - Accidental floral spiral pattern variant
|
||||||
|
*/
|
||||||
|
class FloralAccident extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
|
||||||
|
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
|
||||||
|
{ type: 'color', defaultValue: '#4287f5', property: 'colour' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(sides, width, colour) {
|
||||||
|
super();
|
||||||
|
this.sides = sides;
|
||||||
|
this.width = width;
|
||||||
|
this.colour = colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 100);
|
||||||
|
const rot = Math.round((this.sides - 2) * 180 / this.sides * 2);
|
||||||
|
const piv = 360 / this.sides;
|
||||||
|
let stt = 0.5 * Math.PI - rad(rot);
|
||||||
|
let end = 0;
|
||||||
|
const n = this.width / ((this.width / 10) * (this.width / 10));
|
||||||
|
|
||||||
|
for (let i = 1; i < this.sides + 1; i++) {
|
||||||
|
end = stt + rad(rot);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
centerX + Math.cos(rad(90 + piv * i + rotation)) * this.width,
|
||||||
|
centerY + Math.sin(rad(90 + piv * i + rotation)) * this.width,
|
||||||
|
this.width,
|
||||||
|
stt - (stt - end + rad(rotation)) / 2,
|
||||||
|
end + rad(n),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = this.colour;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
centerX + Math.cos(rad(90 + piv * i - rotation)) * this.width,
|
||||||
|
centerY + Math.sin(rad(90 + piv * i - rotation)) * this.width,
|
||||||
|
this.width,
|
||||||
|
stt,
|
||||||
|
end - (end - stt - rad(rotation)) / 2 + rad(n),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = this.colour;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
stt = end + -(rad(rot - piv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('FloralAccident', FloralAccident);
|
||||||
40
docs/js/shapes/FloralPhyllo.js
Normal file
40
docs/js/shapes/FloralPhyllo.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* FloralPhyllo - Phyllotaxis-based floral pattern with eyelid shapes
|
||||||
|
*/
|
||||||
|
class FloralPhyllo extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 600, defaultValue: 300, property: 'width' },
|
||||||
|
{ type: 'range', min: 1, max: 300, defaultValue: 150, property: 'depth' },
|
||||||
|
{ type: 'range', min: 0, max: 3141, defaultValue: 0, property: 'start' },
|
||||||
|
{ type: 'color', defaultValue: '#4287f5', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(width, depth, start, colour1, colour2) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.depth = depth;
|
||||||
|
this.start = start;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
this.speedMultiplier = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 500) + this.start;
|
||||||
|
const c = 1;
|
||||||
|
|
||||||
|
for (let n = this.depth; n > 0; n -= 1) {
|
||||||
|
const ncolour = LerpHex(this.colour1, this.colour2, n / this.depth);
|
||||||
|
const a = n * rotation / 1000;
|
||||||
|
const r = c * Math.sqrt(n);
|
||||||
|
const x = r * Math.cos(a) + centerX;
|
||||||
|
const y = r * Math.sin(a) + centerY;
|
||||||
|
|
||||||
|
drawEyelid(n * 2.4 + 40, x, y, ncolour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('FloralPhyllo', FloralPhyllo);
|
||||||
37
docs/js/shapes/FloralPhyllo_Accident.js
Normal file
37
docs/js/shapes/FloralPhyllo_Accident.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* FloralPhyllo_Accident - Phyllotaxis pattern with accidental eyelid variation
|
||||||
|
*/
|
||||||
|
class FloralPhyllo_Accident extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
|
||||||
|
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
|
||||||
|
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(sides, width, colour1, colour2) {
|
||||||
|
super();
|
||||||
|
this.sides = sides;
|
||||||
|
this.width = width;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 100);
|
||||||
|
const c = 24;
|
||||||
|
|
||||||
|
for (let n = 0; n < 300; n += 1) {
|
||||||
|
const ncolour = LerpHex(this.colour1, this.colour2, Math.cos(rad(n / 2)));
|
||||||
|
const a = n * (rotation / 1000 + 100);
|
||||||
|
const r = c * Math.sqrt(n);
|
||||||
|
const x = r * Math.cos(a) + centerX;
|
||||||
|
const y = r * Math.sin(a) + centerY;
|
||||||
|
|
||||||
|
drawEyelidAccident(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('FloralPhyllo_Accident', FloralPhyllo_Accident);
|
||||||
42
docs/js/shapes/MaryFace.js
Normal file
42
docs/js/shapes/MaryFace.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* MaryFace - Face overlay with animated eyes
|
||||||
|
*/
|
||||||
|
class MaryFace extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: -110, property: 'x1' },
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: -140, property: 'y1' },
|
||||||
|
{ type: 'range', min: -180, max: 180, defaultValue: 18, property: 'rotate1' },
|
||||||
|
{ type: 'range', min: 0, max: 400, defaultValue: 160, property: 'width1' },
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: 195, property: 'x2' },
|
||||||
|
{ type: 'range', min: -400, max: 400, defaultValue: -30, property: 'y2' },
|
||||||
|
{ type: 'range', min: -180, max: 180, defaultValue: 18, property: 'rotate2' },
|
||||||
|
{ type: 'range', min: 0, max: 400, defaultValue: 160, property: 'width2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(x1, y1, rotate1, width1, x2, y2, rotate2, width2) {
|
||||||
|
super();
|
||||||
|
this.x1 = x1;
|
||||||
|
this.y1 = y1;
|
||||||
|
this.rotate1 = rotate1;
|
||||||
|
this.width1 = width1;
|
||||||
|
this.x2 = x2;
|
||||||
|
this.y2 = y2;
|
||||||
|
this.rotate2 = rotate2;
|
||||||
|
this.width2 = width2;
|
||||||
|
this.eye1 = new EyePrototype(x1, y1, rotate1, 0, width1, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb");
|
||||||
|
this.eye2 = new EyePrototype(x2, y2, rotate2, 0, width2, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb");
|
||||||
|
this.eye3 = new EyePrototype(110, -280, rotate2 + 2, 1, width2, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb");
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = "maryFace.png";
|
||||||
|
|
||||||
|
ctx.drawImage(img, centerX - img.width / 2, centerY - img.height / 2);
|
||||||
|
this.eye1.draw(elapsed);
|
||||||
|
this.eye2.draw(elapsed);
|
||||||
|
this.eye3.draw(elapsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('MaryFace', MaryFace);
|
||||||
52
docs/js/shapes/NewWave.js
Normal file
52
docs/js/shapes/NewWave.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* NewWave - Sine wave pattern radiating from center
|
||||||
|
*/
|
||||||
|
class NewWave extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 300, max: 1600, defaultValue: 342, property: 'width' },
|
||||||
|
{ type: 'range', min: 2, max: 40, defaultValue: 4, property: 'sides' },
|
||||||
|
{ type: 'range', min: 1, max: 100, defaultValue: 1, property: 'step' },
|
||||||
|
{ type: 'range', min: 1, max: 10, defaultValue: 4, property: 'lineWidth' },
|
||||||
|
{ type: 'range', min: 100, max: 1000, defaultValue: 100, property: 'limiter' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(width, sides, step, lineWidth, limiter) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.sides = sides;
|
||||||
|
this.step = step;
|
||||||
|
this.lineWidth = lineWidth;
|
||||||
|
this.limiter = limiter;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * this.speedMultiplier / 400;
|
||||||
|
ctx.lineWidth = this.lineWidth;
|
||||||
|
|
||||||
|
for (let j = 0; j < this.sides; j++) {
|
||||||
|
const radRotation = rad(360 / this.sides * j);
|
||||||
|
const inverter = 1 - (j % 2) * 2;
|
||||||
|
let lastX = centerX;
|
||||||
|
let lastY = centerY;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.width; i += this.step) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(lastX, lastY);
|
||||||
|
ctx.strokeStyle = colourToText(lerpRGB([255, 51, 170], [51, 170, 255], i / this.width));
|
||||||
|
|
||||||
|
const x = i;
|
||||||
|
const y = (Math.sin(-i * inverter / 30 + rotation * inverter) * i / (this.limiter / 100));
|
||||||
|
const xRotated = x * Math.cos(radRotation) - y * Math.sin(radRotation);
|
||||||
|
const yRotated = x * Math.sin(radRotation) + y * Math.cos(radRotation);
|
||||||
|
|
||||||
|
lastX = centerX + xRotated;
|
||||||
|
lastY = centerY + yRotated;
|
||||||
|
ctx.lineTo(centerX + xRotated, centerY + yRotated);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('NewWave', NewWave);
|
||||||
53
docs/js/shapes/Nodal_expanding.js
Normal file
53
docs/js/shapes/Nodal_expanding.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Nodal_expanding - Expanding nodal pattern with color gradient
|
||||||
|
*/
|
||||||
|
class Nodal_expanding extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 100, defaultValue: 5, property: 'expand' },
|
||||||
|
{ type: 'range', min: 1, max: 1000, defaultValue: 150, property: 'points' },
|
||||||
|
{ type: 'range', min: 1, max: 360, defaultValue: 0, property: 'start' },
|
||||||
|
{ type: 'range', min: 1, max: 10, defaultValue: 6, property: 'line_width' },
|
||||||
|
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
|
||||||
|
{ type: 'range', min: 0, max: 10, defaultValue: 5, property: 'colour_change' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(expand, points, start, line_width, colour1, colour2, colour_change) {
|
||||||
|
super();
|
||||||
|
this.expand = expand;
|
||||||
|
this.points = points;
|
||||||
|
this.start = start;
|
||||||
|
this.line_width = line_width;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
this.colour_change = colour_change;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 1000);
|
||||||
|
const angle = (360 / 3000 * rotation) + this.start;
|
||||||
|
let length = this.expand;
|
||||||
|
|
||||||
|
for (let z = 1; z <= this.points; z++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
const ncolour = LerpHex(this.colour1, this.colour2, z / this.points);
|
||||||
|
|
||||||
|
ctx.moveTo(
|
||||||
|
centerX + (Math.cos(rad(angle * (z - 1) + 0)) * (length - this.expand)),
|
||||||
|
centerY + (Math.sin(rad(angle * (z - 1) + 0)) * (length - this.expand))
|
||||||
|
);
|
||||||
|
ctx.lineTo(
|
||||||
|
centerX + (Math.cos(rad(angle * z + 0)) * length),
|
||||||
|
centerY + (Math.sin(rad(angle * z + 0)) * length)
|
||||||
|
);
|
||||||
|
length += this.expand;
|
||||||
|
ctx.lineWidth = this.line_width;
|
||||||
|
ctx.strokeStyle = ncolour;
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('Nodal_expanding', Nodal_expanding);
|
||||||
95
docs/js/shapes/Phyllotaxis.js
Normal file
95
docs/js/shapes/Phyllotaxis.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/**
|
||||||
|
* Phyllotaxis - Classic phyllotaxis pattern with multiple wave modes
|
||||||
|
*/
|
||||||
|
class Phyllotaxis extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 40, defaultValue: 24, property: 'width' },
|
||||||
|
{ type: 'range', min: 1, max: 40, defaultValue: 10, property: 'size' },
|
||||||
|
{ type: 'range', min: 1, max: 40, defaultValue: 4, property: 'sizeMin' },
|
||||||
|
{ type: 'range', min: 0, max: 3141, defaultValue: 0, property: 'start' },
|
||||||
|
{ type: 'range', min: 1, max: 10000, defaultValue: 300, property: 'nMax' },
|
||||||
|
{ type: 'range', min: 0, max: 2, defaultValue: 0, property: 'wave' },
|
||||||
|
{ type: 'range', min: 1, max: 12, defaultValue: 2, property: 'spiralProngs' },
|
||||||
|
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(width, size, sizeMin, start, nMax, wave, spiralProngs, colour1, colour2) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.size = size;
|
||||||
|
this.sizeMin = sizeMin;
|
||||||
|
this.start = start;
|
||||||
|
this.nMax = nMax;
|
||||||
|
this.wave = wave;
|
||||||
|
this.spiralProngs = spiralProngs;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWave(angle) {
|
||||||
|
angle /= 1000;
|
||||||
|
const startColor = [45, 129, 252];
|
||||||
|
const endColor = [252, 3, 98];
|
||||||
|
const distanceMultiplier = 3;
|
||||||
|
const maxIterations = this.nMax;
|
||||||
|
|
||||||
|
for (let n = 0; n < maxIterations; n++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
const nColor = lerpRGB(startColor, endColor, Math.cos(rad(n / 2)));
|
||||||
|
const nAngle = n * angle + Math.sin(rad(n * 1 + angle * 40000)) / 2;
|
||||||
|
const radius = distanceMultiplier * n;
|
||||||
|
const xCoord = radius * Math.cos(nAngle) + centerX;
|
||||||
|
const yCoord = radius * Math.sin(nAngle) + centerY;
|
||||||
|
ctx.arc(xCoord, yCoord, this.size, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = colourToText(nColor);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSpiral(angle) {
|
||||||
|
angle /= 5000;
|
||||||
|
const startColor = [45, 129, 252];
|
||||||
|
const endColor = [252, 3, 98];
|
||||||
|
const distanceMultiplier = 2;
|
||||||
|
const maxIterations = 1000;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX, centerY);
|
||||||
|
for (let n = 0; n < maxIterations; n++) {
|
||||||
|
const nAngle = n * angle + Math.sin(angle * n * this.spiralProngs);
|
||||||
|
const radius = distanceMultiplier * n;
|
||||||
|
const xCoord = radius * Math.cos(nAngle) + centerX;
|
||||||
|
const yCoord = radius * Math.sin(nAngle) + centerY;
|
||||||
|
ctx.lineTo(xCoord, yCoord);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 300) + this.start;
|
||||||
|
const sizeMultiplier = this.nMax / this.size + (5 - 3);
|
||||||
|
|
||||||
|
if (this.wave === 1) {
|
||||||
|
this.drawWave(rotation);
|
||||||
|
} else if (this.wave === 2) {
|
||||||
|
this.drawSpiral(rotation);
|
||||||
|
} else {
|
||||||
|
for (let n = 0; n < this.nMax; n += 1) {
|
||||||
|
const ncolour = LerpHex(this.colour1, this.colour2, n / this.nMax);
|
||||||
|
const a = n * (rotation / 1000);
|
||||||
|
const r = this.width * Math.sqrt(n);
|
||||||
|
const x = r * Math.cos(a) + centerX;
|
||||||
|
const y = r * Math.sin(a) + centerY;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(x, y, (n / sizeMultiplier) + this.sizeMin, 0, 2 * Math.PI);
|
||||||
|
ctx.fillStyle = ncolour;
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('Phyllotaxis', Phyllotaxis);
|
||||||
50
docs/js/shapes/PolyTwistColourWidth.js
Normal file
50
docs/js/shapes/PolyTwistColourWidth.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* PolyTwistColourWidth - Twisted polygon with color gradient
|
||||||
|
*/
|
||||||
|
class PolyTwistColourWidth extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 3, max: 10, defaultValue: 5, property: 'sides' },
|
||||||
|
{ type: 'range', min: 400, max: 2000, defaultValue: 400, property: 'width' },
|
||||||
|
{ type: 'range', min: 2, max: 5, defaultValue: 5, property: 'line_width' },
|
||||||
|
{ type: 'range', min: 1, max: 100, defaultValue: 50, property: 'depth' },
|
||||||
|
{ type: 'range', min: -180, max: 180, defaultValue: -90, property: 'rotation' },
|
||||||
|
{ type: 'color', defaultValue: '#4287f5', property: 'colour1' },
|
||||||
|
{ type: 'color', defaultValue: '#42f57b', property: 'colour2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(sides, width, line_width, depth, rotation, colour1, colour2) {
|
||||||
|
super();
|
||||||
|
this.sides = sides;
|
||||||
|
this.width = width;
|
||||||
|
this.line_width = line_width;
|
||||||
|
this.depth = depth;
|
||||||
|
this.rotation = rotation;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
this.colour2 = colour2;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 100);
|
||||||
|
|
||||||
|
let out_angle = 0;
|
||||||
|
const innerAngle = 180 - ((this.sides - 2) * 180) / this.sides;
|
||||||
|
const scopeAngle = rotation - (innerAngle * Math.floor(rotation / innerAngle));
|
||||||
|
|
||||||
|
if (scopeAngle < innerAngle / 2) {
|
||||||
|
out_angle = innerAngle / (2 * Math.cos((2 * Math.PI * scopeAngle) / (3 * innerAngle))) - innerAngle / 2;
|
||||||
|
} else {
|
||||||
|
out_angle = -innerAngle / (2 * Math.cos(((2 * Math.PI) / 3) - ((2 * Math.PI * scopeAngle) / (3 * innerAngle)))) + (innerAngle * 3) / 2;
|
||||||
|
}
|
||||||
|
const minWidth = Math.sin(rad(innerAngle / 2)) * (0.5 / Math.tan(rad(innerAngle / 2))) * 2;
|
||||||
|
const widthMultiplier = minWidth / Math.sin(Math.PI / 180 * (90 + innerAngle / 2 - out_angle + innerAngle * Math.floor(out_angle / innerAngle)));
|
||||||
|
|
||||||
|
for (let i = 0; i < this.depth; i++) {
|
||||||
|
const fraction = i / this.depth;
|
||||||
|
const ncolour = LerpHex(this.colour1, this.colour2, fraction);
|
||||||
|
DrawPolygon(this.sides, this.width * widthMultiplier ** i, out_angle * i + this.rotation, ncolour, this.line_width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('PolyTwistColourWidth', PolyTwistColourWidth);
|
||||||
289
docs/js/shapes/RaysInShape.js
Normal file
289
docs/js/shapes/RaysInShape.js
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
/**
|
||||||
|
* RaysInShape - Rays bouncing within a box with center-returning trails
|
||||||
|
*/
|
||||||
|
class RaysInShape extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 50, max: 1000, defaultValue: 500, property: 'rays', callback: (instance, newValue) => instance.setRays(newValue) },
|
||||||
|
{ type: 'range', min: 1, max: 30, defaultValue: 2, property: 'speed' },
|
||||||
|
{ type: 'checkbox', defaultValue: true, property: 'doesWave' },
|
||||||
|
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedVertRate' },
|
||||||
|
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedHorrRate' },
|
||||||
|
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedVert' },
|
||||||
|
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedHorr' },
|
||||||
|
{ type: 'range', min: 10, max: 2000, defaultValue: 800, property: 'boxSize' },
|
||||||
|
{ type: 'range', min: 1, max: 80, defaultValue: 5, property: 'trailLength' },
|
||||||
|
{ type: 'range', min: 1, max: 500, defaultValue: 5, property: 'lineWidth' },
|
||||||
|
{ type: 'checkbox', defaultValue: false, property: 'fade' },
|
||||||
|
{ type: 'color', defaultValue: '#43dbad', property: 'colourFree' },
|
||||||
|
{ type: 'color', defaultValue: '#f05c79', property: 'colourContained' },
|
||||||
|
{ type: 'header', text: '--CollisionBox---' },
|
||||||
|
{ type: 'checkbox', defaultValue: false, property: 'boxVisible' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(rays, speed, doesWave, speedVertRate, speedHorrRate, speedVert, speedHorr, boxSize, trailLength = 50, lineWidth, fade, colourFree, colourContained, boxVisible) {
|
||||||
|
super();
|
||||||
|
this.rays = rays;
|
||||||
|
this.speed = speed;
|
||||||
|
this.speedVert = speedVert;
|
||||||
|
this.speedHorr = speedHorr;
|
||||||
|
this.boxSize = boxSize;
|
||||||
|
this.trailLength = trailLength;
|
||||||
|
this.rayObjects = [];
|
||||||
|
this.centerRays = [];
|
||||||
|
this.lineWidth = lineWidth;
|
||||||
|
this.boxVisible = boxVisible;
|
||||||
|
this.doesWave = doesWave;
|
||||||
|
this.colourFree = colourFree;
|
||||||
|
this.colourContained = colourContained;
|
||||||
|
this.speedHorrRate = speedHorrRate;
|
||||||
|
this.speedVertRate = speedVertRate;
|
||||||
|
this.fade = fade;
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeControls(controlManager) {
|
||||||
|
super.initializeControls(controlManager);
|
||||||
|
this.prepareRayObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareRayObjects() {
|
||||||
|
this.rayObjects = [];
|
||||||
|
for (let i = 0; i < this.rays; i++) {
|
||||||
|
const angle = (360 / this.rays) * i;
|
||||||
|
this.rayObjects.push({
|
||||||
|
angle: angle,
|
||||||
|
lastX: centerX,
|
||||||
|
lastY: centerY,
|
||||||
|
positions: [{ x: centerX, y: centerY, angle: angle }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.centerRays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
createCenterRay(x, y) {
|
||||||
|
const dx = centerX - x;
|
||||||
|
const dy = centerY - y;
|
||||||
|
const angleToCenter = Math.atan2(dy, dx) * 180 / Math.PI;
|
||||||
|
|
||||||
|
this.centerRays.push({
|
||||||
|
positions: [{ x: x, y: y }],
|
||||||
|
angle: angleToCenter,
|
||||||
|
reachedCenter: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCenterRays(deltaTime) {
|
||||||
|
const centerThreshold = 5;
|
||||||
|
const maxDistance = 2000;
|
||||||
|
|
||||||
|
for (let i = 0; i < this.centerRays.length; i++) {
|
||||||
|
const ray = this.centerRays[i];
|
||||||
|
|
||||||
|
if (ray.reachedCenter) {
|
||||||
|
if (ray.positions.length > 0) {
|
||||||
|
ray.positions.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ray.positions.length <= 1) {
|
||||||
|
this.centerRays.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const currentPos = ray.positions[ray.positions.length - 1];
|
||||||
|
|
||||||
|
const dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
|
||||||
|
const dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
|
||||||
|
const newX = currentPos.x + dx;
|
||||||
|
const newY = currentPos.y + dy;
|
||||||
|
|
||||||
|
const distFromOrigin = Math.sqrt(
|
||||||
|
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distFromOrigin > maxDistance) {
|
||||||
|
this.centerRays.splice(i, 1);
|
||||||
|
i--;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ray.positions.push({ x: newX, y: newY });
|
||||||
|
|
||||||
|
const distToCenter = Math.sqrt(
|
||||||
|
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (distToCenter <= centerThreshold) {
|
||||||
|
ray.reachedCenter = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ray.positions.length > this.trailLength) {
|
||||||
|
ray.positions.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 1; j < ray.positions.length; j++) {
|
||||||
|
const prev = ray.positions[j - 1];
|
||||||
|
const curr = ray.positions[j];
|
||||||
|
|
||||||
|
let alpha = 1;
|
||||||
|
if (this.fade) {
|
||||||
|
alpha = (j / ray.positions.length) * 0.8 + 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(prev.x, prev.y);
|
||||||
|
ctx.lineTo(curr.x, curr.y);
|
||||||
|
|
||||||
|
const col = hexToRgb(this.colourFree);
|
||||||
|
ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setRays(newValue) {
|
||||||
|
this.rays = newValue;
|
||||||
|
this.prepareRayObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed, deltaTime) {
|
||||||
|
deltaTime *= this.speedMultiplier / 100;
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
|
||||||
|
if (this.doesWave) {
|
||||||
|
const vertRate = this.speedVertRate / 100;
|
||||||
|
const horrRate = this.speedHorrRate / 100;
|
||||||
|
this.speedVert = Math.sin(elapsed / 10 * vertRate) * 85 + 100;
|
||||||
|
this.speedHorr = Math.sin(elapsed / 10 * horrRate) * 85 + 100;
|
||||||
|
updateControlInput(this.speedVert, "speedVert");
|
||||||
|
updateControlInput(this.speedHorr, "speedHorr");
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxLeft = centerX - this.boxSize / 2;
|
||||||
|
const boxRight = centerX + this.boxSize / 2;
|
||||||
|
const boxTop = centerY - this.boxSize / 2;
|
||||||
|
const boxBottom = centerY + this.boxSize / 2;
|
||||||
|
|
||||||
|
if (this.boxVisible) {
|
||||||
|
ctx.strokeStyle = "white";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(boxLeft, boxTop, this.boxSize, this.boxSize);
|
||||||
|
}
|
||||||
|
ctx.lineWidth = this.lineWidth;
|
||||||
|
|
||||||
|
for (let j = 0; j < this.rayObjects.length; j++) {
|
||||||
|
const ray = this.rayObjects[j];
|
||||||
|
const currentPos = ray.positions[ray.positions.length - 1];
|
||||||
|
|
||||||
|
let dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
|
||||||
|
let dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
|
||||||
|
let newX = currentPos.x + dx;
|
||||||
|
let newY = currentPos.y + dy;
|
||||||
|
let collisionType = null;
|
||||||
|
const oldAngle = ray.angle;
|
||||||
|
|
||||||
|
if (newX < boxLeft || newX > boxRight) {
|
||||||
|
const collisionX = newX < boxLeft ? boxLeft : boxRight;
|
||||||
|
const collisionRatio = (collisionX - currentPos.x) / dx;
|
||||||
|
const collisionY = currentPos.y + dy * collisionRatio;
|
||||||
|
|
||||||
|
ray.positions.push({
|
||||||
|
x: collisionX,
|
||||||
|
y: collisionY,
|
||||||
|
angle: oldAngle,
|
||||||
|
collision: 'horizontal'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createCenterRay(collisionX, collisionY);
|
||||||
|
|
||||||
|
ray.angle = 180 - ray.angle;
|
||||||
|
ray.angle = ((ray.angle % 360) + 360) % 360;
|
||||||
|
|
||||||
|
const remainingRatio = 1 - collisionRatio;
|
||||||
|
dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
|
||||||
|
dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
|
||||||
|
newX = collisionX + dx;
|
||||||
|
newY = collisionY + dy;
|
||||||
|
collisionType = 'horizontal';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newY < boxTop || newY > boxBottom) {
|
||||||
|
if (collisionType === null) {
|
||||||
|
const collisionY = newY < boxTop ? boxTop : boxBottom;
|
||||||
|
const collisionRatio = (collisionY - currentPos.y) / dy;
|
||||||
|
const collisionX = currentPos.x + dx * collisionRatio;
|
||||||
|
|
||||||
|
ray.positions.push({
|
||||||
|
x: collisionX,
|
||||||
|
y: collisionY,
|
||||||
|
angle: oldAngle,
|
||||||
|
collision: 'vertical'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createCenterRay(collisionX, collisionY);
|
||||||
|
|
||||||
|
ray.angle = 360 - ray.angle;
|
||||||
|
ray.angle = ((ray.angle % 360) + 360) % 360;
|
||||||
|
|
||||||
|
const remainingRatio = 1 - collisionRatio;
|
||||||
|
dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
|
||||||
|
dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
|
||||||
|
newX = collisionX + dx;
|
||||||
|
newY = collisionY + dy;
|
||||||
|
} else {
|
||||||
|
newX = Math.max(boxLeft, Math.min(newX, boxRight));
|
||||||
|
newY = Math.max(boxTop, Math.min(newY, boxBottom));
|
||||||
|
ray.positions.push({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
angle: ray.angle,
|
||||||
|
collision: 'corner'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.createCenterRay(newX, newY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newX = Math.max(boxLeft, Math.min(newX, boxRight));
|
||||||
|
newY = Math.max(boxTop, Math.min(newY, boxBottom));
|
||||||
|
|
||||||
|
if (collisionType === null) {
|
||||||
|
ray.positions.push({
|
||||||
|
x: newX,
|
||||||
|
y: newY,
|
||||||
|
angle: ray.angle
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
while (ray.positions.length > this.trailLength) {
|
||||||
|
ray.positions.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i < ray.positions.length; i++) {
|
||||||
|
const prev = ray.positions[i - 1];
|
||||||
|
const curr = ray.positions[i];
|
||||||
|
|
||||||
|
let alpha = 1;
|
||||||
|
if (this.fade) {
|
||||||
|
alpha = (i / ray.positions.length) * 0.8 + 0.2;
|
||||||
|
}
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(prev.x, prev.y);
|
||||||
|
ctx.lineTo(curr.x, curr.y);
|
||||||
|
|
||||||
|
if (curr.collision) {
|
||||||
|
ctx.strokeStyle = `rgba(255, 255, 0, ${alpha})`;
|
||||||
|
} else {
|
||||||
|
const col = hexToRgb(this.colourContained);
|
||||||
|
ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCenterRays(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('RaysInShape', RaysInShape);
|
||||||
60
docs/js/shapes/Spiral1.js
Normal file
60
docs/js/shapes/Spiral1.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* Spiral1 - Dual-direction spiral pattern
|
||||||
|
*/
|
||||||
|
class Spiral1 extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
|
||||||
|
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
|
||||||
|
{ type: 'color', defaultValue: '#4287f5', property: 'colour' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(sides, width, colour) {
|
||||||
|
super();
|
||||||
|
this.sides = sides;
|
||||||
|
this.width = width;
|
||||||
|
this.colour = colour;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 100);
|
||||||
|
const rot = Math.round((this.sides - 2) * 180 / this.sides * 2);
|
||||||
|
const piv = 360 / this.sides;
|
||||||
|
let stt = 0.5 * Math.PI - rad(rot);
|
||||||
|
let end = 0;
|
||||||
|
const n = this.width / ((this.width / 10) * (this.width / 10));
|
||||||
|
|
||||||
|
for (let i = 1; i < this.sides + 1; i++) {
|
||||||
|
end = stt + rad(rot);
|
||||||
|
ctx.lineWidth = 5;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
centerX + Math.cos(rad(90 + piv * i + rotation)) * this.width,
|
||||||
|
centerY + Math.sin(rad(90 + piv * i + rotation)) * this.width,
|
||||||
|
this.width,
|
||||||
|
stt + rad(rotation) - (stt - end) / 2,
|
||||||
|
end + rad(rotation) + rad(n),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = this.colour;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(
|
||||||
|
centerX + Math.cos(rad(90 + piv * i - rotation)) * this.width,
|
||||||
|
centerY + Math.sin(rad(90 + piv * i - rotation)) * this.width,
|
||||||
|
this.width,
|
||||||
|
stt - rad(rotation),
|
||||||
|
end - (end - stt) / 2 + rad(n) - rad(rotation),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
ctx.strokeStyle = this.colour;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
stt = end + -(rad(rot - piv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('Spiral1', Spiral1);
|
||||||
42
docs/js/shapes/SquareTwist_angle.js
Normal file
42
docs/js/shapes/SquareTwist_angle.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* SquareTwist_angle - Twisted square pattern with angle-based scaling
|
||||||
|
*/
|
||||||
|
class SquareTwist_angle extends BaseShape {
|
||||||
|
static config = [
|
||||||
|
{ type: 'range', min: 1, max: 800, defaultValue: 400, property: 'width' },
|
||||||
|
{ type: 'range', min: 1, max: 10, defaultValue: 1, property: 'line_width' },
|
||||||
|
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(width, line_width, colour1) {
|
||||||
|
super();
|
||||||
|
this.width = width;
|
||||||
|
this.line_width = line_width;
|
||||||
|
this.colour1 = colour1;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawSquare(angle, size, colour) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(centerX, centerY);
|
||||||
|
ctx.rotate(rad(angle + 180));
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = colour;
|
||||||
|
ctx.lineWidth = this.line_width;
|
||||||
|
ctx.rect(-size / 2, -size / 2, size, size);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(elapsed) {
|
||||||
|
this.updateFilters(elapsed);
|
||||||
|
const rotation = elapsed * (this.speedMultiplier / 100);
|
||||||
|
const out_angle = rotation;
|
||||||
|
const widthMultiplier = 1 / (2 * Math.sin(Math.PI / 180 * (130 - out_angle + 90 * Math.floor(out_angle / 90)))) + 0.5;
|
||||||
|
|
||||||
|
for (let i = 0; i < 25; i++) {
|
||||||
|
this.drawSquare(rotation * i, this.width * widthMultiplier ** i, this.colour1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shapeRegistry.register('SquareTwist_angle', SquareTwist_angle);
|
||||||
290
docs/js/utils/helpers.js
Normal file
290
docs/js/utils/helpers.js
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
/**
|
||||||
|
* Drawing utility functions for the animation framework
|
||||||
|
* These are pure functions used by shapes for rendering
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert degrees to radians
|
||||||
|
* @param {number} degrees - Angle in degrees
|
||||||
|
* @returns {number} Angle in radians
|
||||||
|
*/
|
||||||
|
function rad(degrees) {
|
||||||
|
return (degrees * Math.PI) / 180;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert RGB array to CSS color string
|
||||||
|
* @param {number[]} colour - Array of [r, g, b] values
|
||||||
|
* @returns {string} CSS color string
|
||||||
|
*/
|
||||||
|
function colourToText(colour) {
|
||||||
|
return "rgb(" + colour[0] + "," + colour[1] + "," + colour[2] + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert hex color to RGB object
|
||||||
|
* @param {string} hex - Hex color string (e.g., '#ff0000')
|
||||||
|
* @returns {{r: number, g: number, b: number}|null} RGB object or null
|
||||||
|
*/
|
||||||
|
function hexToRgb(hex) {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
|
return result ? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16)
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linear interpolation between two hex colors
|
||||||
|
* @param {string} a - Start hex color
|
||||||
|
* @param {string} b - End hex color
|
||||||
|
* @param {number} amount - Interpolation amount (0-1)
|
||||||
|
* @returns {string} Interpolated hex color
|
||||||
|
*/
|
||||||
|
function LerpHex(a, b, amount) {
|
||||||
|
const ah = parseInt(a.replace(/#/g, ""), 16);
|
||||||
|
const ar = ah >> 16;
|
||||||
|
const ag = (ah >> 8) & 0xff;
|
||||||
|
const ab = ah & 0xff;
|
||||||
|
const bh = parseInt(b.replace(/#/g, ""), 16);
|
||||||
|
const br = bh >> 16;
|
||||||
|
const bg = (bh >> 8) & 0xff;
|
||||||
|
const bb = bh & 0xff;
|
||||||
|
const rr = ar + amount * (br - ar);
|
||||||
|
const rg = ag + amount * (bg - ag);
|
||||||
|
const rb = ab + amount * (bb - ab);
|
||||||
|
|
||||||
|
return "#" + (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linear interpolation between two RGB arrays
|
||||||
|
* @param {number[]} a - Start RGB array [r, g, b]
|
||||||
|
* @param {number[]} b - End RGB array [r, g, b]
|
||||||
|
* @param {number} t - Interpolation amount (0-1)
|
||||||
|
* @returns {number[]} Interpolated RGB array
|
||||||
|
*/
|
||||||
|
function lerpRGB(a, b, t) {
|
||||||
|
const result = [0, 0, 0];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
result[i] = (1 - t) * a[i] + t * b[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy LerpRGB function (handles negative t)
|
||||||
|
* @param {number[]} a - Start RGB array
|
||||||
|
* @param {number[]} b - End RGB array
|
||||||
|
* @param {number} t - Interpolation amount
|
||||||
|
* @returns {number[]} Interpolated RGB array
|
||||||
|
*/
|
||||||
|
function LerpRGB(a, b, t) {
|
||||||
|
if (t < 0) {
|
||||||
|
t *= -1;
|
||||||
|
}
|
||||||
|
const newColor = [0, 0, 0];
|
||||||
|
newColor[0] = a[0] + (b[0] - a[0]) * t;
|
||||||
|
newColor[1] = a[1] + (b[1] - a[1]) * t;
|
||||||
|
newColor[2] = a[2] + (b[2] - a[2]) * t;
|
||||||
|
return newColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a wave-normalized value (0-1 range using sine)
|
||||||
|
* @param {number} x - Current position
|
||||||
|
* @param {number} max - Maximum position
|
||||||
|
* @returns {number} Normalized value (0-1)
|
||||||
|
*/
|
||||||
|
function waveNormal(x, max) {
|
||||||
|
return Math.sin((x / max) * Math.PI * 2 - max * (Math.PI / (max * 2))) / 2 + 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotate a point around origin
|
||||||
|
* @param {number} x - X coordinate
|
||||||
|
* @param {number} y - Y coordinate
|
||||||
|
* @param {number} rotation - Rotation angle in degrees
|
||||||
|
* @returns {number[]} Rotated [x, y] coordinates
|
||||||
|
*/
|
||||||
|
function rotatePoint(x, y, rotation) {
|
||||||
|
const nCos = Math.cos(rad(rotation));
|
||||||
|
const nSin = Math.sin(rad(rotation));
|
||||||
|
const newX = x * nCos - y * nSin;
|
||||||
|
const newY = y * nCos + x * nSin;
|
||||||
|
return [newX, newY];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a regular polygon
|
||||||
|
* @param {number} sides - Number of sides
|
||||||
|
* @param {number} width - Radius of polygon
|
||||||
|
* @param {number} rotation - Rotation in degrees
|
||||||
|
* @param {string} colour - Stroke color
|
||||||
|
* @param {number} line_width - Line width
|
||||||
|
*/
|
||||||
|
function DrawPolygon(sides, width, rotation, colour, line_width) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(
|
||||||
|
centerX + width * Math.cos((rotation * Math.PI) / 180),
|
||||||
|
centerY + width * Math.sin((rotation * Math.PI) / 180)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 1; i <= sides; i += 1) {
|
||||||
|
ctx.lineTo(
|
||||||
|
centerX + width * Math.cos((i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180),
|
||||||
|
centerY + width * Math.sin((i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctx.strokeStyle = colour;
|
||||||
|
ctx.lineWidth = line_width;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw an eyelid shape
|
||||||
|
* @param {number} width - Width of the eyelid
|
||||||
|
* @param {number} x1 - X position
|
||||||
|
* @param {number} y1 - Y position
|
||||||
|
* @param {string} colour - Fill color
|
||||||
|
*/
|
||||||
|
function drawEyelid(width, x1, y1, colour) {
|
||||||
|
x1 -= centerX;
|
||||||
|
y1 -= centerY;
|
||||||
|
|
||||||
|
const angle = Math.atan2(y1, x1);
|
||||||
|
const cosAngle = Math.cos(angle);
|
||||||
|
const sinAngle = Math.sin(angle);
|
||||||
|
|
||||||
|
const x2 = cosAngle * width;
|
||||||
|
const y2 = sinAngle * width;
|
||||||
|
|
||||||
|
const x3Old = width / 2;
|
||||||
|
const y3Old = width / 2;
|
||||||
|
const x4Old = width / 2;
|
||||||
|
const y4Old = -width / 2;
|
||||||
|
|
||||||
|
const x3 = x3Old * cosAngle - y3Old * sinAngle;
|
||||||
|
const y3 = x3Old * sinAngle + y3Old * cosAngle;
|
||||||
|
const x4 = x4Old * cosAngle - y4Old * sinAngle;
|
||||||
|
const y4 = x4Old * sinAngle + y4Old * cosAngle;
|
||||||
|
|
||||||
|
x1 += centerX;
|
||||||
|
y1 += centerY;
|
||||||
|
const x2Final = x2 + x1;
|
||||||
|
const y2Final = y2 + y1;
|
||||||
|
const x3Final = x3 + x1;
|
||||||
|
const y3Final = y3 + y1;
|
||||||
|
const x4Final = x4 + x1;
|
||||||
|
const y4Final = y4 + y1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x3Final, y3Final, x2Final, y2Final);
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x4Final, y4Final, x2Final, y2Final);
|
||||||
|
ctx.fillStyle = colour;
|
||||||
|
ctx.fill();
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = "black";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw an accident-style eyelid shape
|
||||||
|
* @param {number} x1 - X position
|
||||||
|
* @param {number} y1 - Y position
|
||||||
|
*/
|
||||||
|
function drawEyelidAccident(x1, y1) {
|
||||||
|
const leafWidth = 120;
|
||||||
|
const leafHeight = 60;
|
||||||
|
x1 -= centerX;
|
||||||
|
y1 -= centerY;
|
||||||
|
let angle = Math.atan(y1 / x1);
|
||||||
|
angle = Math.abs(angle);
|
||||||
|
|
||||||
|
const x2Old = leafWidth;
|
||||||
|
const y2Old = 0;
|
||||||
|
const x3Old = leafWidth / 2;
|
||||||
|
const y3Old = leafHeight / 2;
|
||||||
|
const x4Old = leafWidth / 2;
|
||||||
|
const y4Old = -leafHeight / 2;
|
||||||
|
|
||||||
|
const x2 = x2Old * Math.cos(angle) - y2Old * Math.sin(angle);
|
||||||
|
const y2 = x2Old * Math.sin(angle) + y2Old * Math.cos(angle);
|
||||||
|
const x3 = x3Old * Math.cos(angle) - y3Old * Math.sin(angle);
|
||||||
|
const y3 = x3Old * Math.sin(angle) + y3Old * Math.cos(angle);
|
||||||
|
const x4 = x4Old * Math.cos(angle) - y4Old * Math.sin(angle);
|
||||||
|
const y4 = x4Old * Math.sin(angle) + y4Old * Math.cos(angle);
|
||||||
|
|
||||||
|
x1 += centerX;
|
||||||
|
y1 += centerY;
|
||||||
|
const x2f = x2 + x1;
|
||||||
|
const y2f = y2 + y1;
|
||||||
|
const x3f = x3 + x1;
|
||||||
|
const y3f = y3 + y1;
|
||||||
|
const x4f = x4 + x1;
|
||||||
|
const y4f = y4 + y1;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x3f, y3f, x2f, y2f);
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x4f, y4f, x2f, y2f);
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x3f, y3f, x2f, y2f);
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.quadraticCurveTo(x4f, y4f, x2f, y2f);
|
||||||
|
ctx.strokeStyle = "orange";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the canvas and fill with black
|
||||||
|
*/
|
||||||
|
function render_clear() {
|
||||||
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
ctx.fillStyle = "black";
|
||||||
|
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw crosshairs at center (for debugging)
|
||||||
|
* @param {number} width - Length of crosshair lines
|
||||||
|
*/
|
||||||
|
function drawCenter(width) {
|
||||||
|
ctx.strokeStyle = "pink";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX - width, centerY);
|
||||||
|
ctx.lineTo(centerX + width, centerY);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(centerX, centerY - width);
|
||||||
|
ctx.lineTo(centerX, centerY + width);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a control input's displayed value
|
||||||
|
* @param {number} value - New value
|
||||||
|
* @param {string} controlName - Name of the control property
|
||||||
|
*/
|
||||||
|
function updateControlInput(value, controlName) {
|
||||||
|
const elementSlider = document.querySelector('input[type="range"][id="el' + controlName + '"]');
|
||||||
|
if (elementSlider) {
|
||||||
|
elementSlider.value = value;
|
||||||
|
const elementSliderText = document.getElementById(`elText${controlName}`);
|
||||||
|
if (elementSliderText) {
|
||||||
|
elementSliderText.innerText = `${controlName}: ${Math.round(value)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19448
docs/react/package-lock.json
generated
19448
docs/react/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "animate-react",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@react-three/drei": "^9.92.7",
|
|
||||||
"@react-three/fiber": "^8.15.12",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"three": "^0.159.0",
|
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"zustand": "^4.4.7"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.2%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"react-scripts": "5.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Animate - Ported to React and Three.js"
|
|
||||||
/>
|
|
||||||
<title>Animate - React</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Canvas } from '@react-three/fiber';
|
|
||||||
import { OrbitControls } from '@react-three/drei';
|
|
||||||
import Toolbar from './components/Toolbar';
|
|
||||||
import AnimationScene from './components/AnimationScene';
|
|
||||||
import useAnimationStore from './store/animationStore';
|
|
||||||
|
|
||||||
function App() {
|
|
||||||
const [showToolbar, setShowToolbar] = useState(true);
|
|
||||||
const { selectedAnimation, setSelectedAnimation, animations } = useAnimationStore();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e) => {
|
|
||||||
if (e.key === 'p' || e.key === 'P') {
|
|
||||||
setShowToolbar(prev => !prev);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="App">
|
|
||||||
<Canvas
|
|
||||||
style={{ background: '#000', width: '100%', height: '100vh', position: 'absolute' }}
|
|
||||||
camera={{ position: [0, 0, 5], fov: 75 }}
|
|
||||||
dpr={[1, 2]}
|
|
||||||
>
|
|
||||||
<ambientLight intensity={0.5} />
|
|
||||||
<AnimationScene />
|
|
||||||
<OrbitControls />
|
|
||||||
</Canvas>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="toggle-button"
|
|
||||||
onClick={() => setShowToolbar(prev => !prev)}
|
|
||||||
>
|
|
||||||
{showToolbar ? 'Hide' : 'Show'} Controls
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Toolbar
|
|
||||||
isVisible={showToolbar}
|
|
||||||
selectedAnimation={selectedAnimation}
|
|
||||||
onAnimationChange={setSelectedAnimation}
|
|
||||||
animations={animations}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
|
||||||
import { useFrame, useThree } from '@react-three/fiber';
|
|
||||||
import * as THREE from 'three';
|
|
||||||
import useAnimationStore from '../store/animationStore';
|
|
||||||
import { PolyTwistColourWidth } from './animations/PolyTwistColourWidth';
|
|
||||||
import { FloralPhyllo } from './animations/FloralPhyllo';
|
|
||||||
import { RaysInShape } from './animations/RaysInShape';
|
|
||||||
|
|
||||||
const AnimationScene = () => {
|
|
||||||
const { selectedAnimation, elapsedTime, rotation, updateAnimation, animations, paused } = useAnimationStore();
|
|
||||||
const lastTimeRef = useRef(0);
|
|
||||||
const sceneRef = useRef();
|
|
||||||
const objectsToDisposeRef = useRef([]);
|
|
||||||
const { viewport, size } = useThree();
|
|
||||||
|
|
||||||
// Helper function to properly dispose of Three.js objects
|
|
||||||
const disposeObjects = () => {
|
|
||||||
if (objectsToDisposeRef.current.length > 0) {
|
|
||||||
objectsToDisposeRef.current.forEach(object => {
|
|
||||||
// Dispose of geometries
|
|
||||||
if (object.geometry) {
|
|
||||||
object.geometry.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispose of materials (could be an array or a single material)
|
|
||||||
if (object.material) {
|
|
||||||
if (Array.isArray(object.material)) {
|
|
||||||
object.material.forEach(material => material.dispose());
|
|
||||||
} else {
|
|
||||||
object.material.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the disposal list
|
|
||||||
objectsToDisposeRef.current = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Clear scene when animation changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (sceneRef.current) {
|
|
||||||
// Dispose of all existing objects first
|
|
||||||
disposeObjects();
|
|
||||||
|
|
||||||
while (sceneRef.current.children.length > 0) {
|
|
||||||
const child = sceneRef.current.children[0];
|
|
||||||
sceneRef.current.remove(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [selectedAnimation]);
|
|
||||||
|
|
||||||
// Cleanup on component unmount
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
disposeObjects();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Update animation on each frame
|
|
||||||
useFrame((state, delta) => {
|
|
||||||
if (!paused) {
|
|
||||||
updateAnimation(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current animation parameters
|
|
||||||
const animationParams = animations[selectedAnimation].parameters;
|
|
||||||
|
|
||||||
// Get elapsed time in seconds
|
|
||||||
const currentTime = elapsedTime;
|
|
||||||
const deltaTime = currentTime - lastTimeRef.current;
|
|
||||||
lastTimeRef.current = currentTime;
|
|
||||||
|
|
||||||
// Clear the scene for a new frame
|
|
||||||
if (sceneRef.current) {
|
|
||||||
// Dispose of previous objects first to prevent memory leaks
|
|
||||||
disposeObjects();
|
|
||||||
|
|
||||||
while (sceneRef.current.children.length > 0) {
|
|
||||||
const child = sceneRef.current.children[0];
|
|
||||||
sceneRef.current.remove(child);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array to collect objects created in this frame for later disposal
|
|
||||||
const newObjects = [];
|
|
||||||
|
|
||||||
// Render the selected animation
|
|
||||||
switch (selectedAnimation) {
|
|
||||||
case 'PolyTwistColourWidth':
|
|
||||||
PolyTwistColourWidth({
|
|
||||||
scene: sceneRef.current,
|
|
||||||
rotation,
|
|
||||||
params: animationParams,
|
|
||||||
viewport,
|
|
||||||
objectsToDispose: newObjects
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'FloralPhyllo':
|
|
||||||
FloralPhyllo({
|
|
||||||
scene: sceneRef.current,
|
|
||||||
rotation,
|
|
||||||
params: animationParams,
|
|
||||||
viewport,
|
|
||||||
objectsToDispose: newObjects
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'RaysInShape':
|
|
||||||
RaysInShape({
|
|
||||||
scene: sceneRef.current,
|
|
||||||
elapsedTime: currentTime,
|
|
||||||
deltaTime,
|
|
||||||
params: animationParams,
|
|
||||||
viewport,
|
|
||||||
objectsToDispose: newObjects
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Default: render nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store objects for disposal in the next frame
|
|
||||||
objectsToDisposeRef.current = newObjects;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group ref={sceneRef} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AnimationScene;
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import useAnimationStore from '../store/animationStore';
|
|
||||||
|
|
||||||
const ControlFilter = ({ filter, property, index }) => {
|
|
||||||
const { updateFilter, removeFilter } = useAnimationStore();
|
|
||||||
|
|
||||||
const handleFilterChange = (filterProperty, value) => {
|
|
||||||
updateFilter(property, index, filterProperty, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="filter">
|
|
||||||
<div className="filter-header">
|
|
||||||
Filter {index + 1}: {filter.min} to {filter.max} @ {filter.frequency.toFixed(2)}Hz
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="control-row">
|
|
||||||
<label>Min:</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className="range-control"
|
|
||||||
value={filter.min}
|
|
||||||
min={-200}
|
|
||||||
max={filter.max}
|
|
||||||
onChange={(e) => handleFilterChange('min', parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
<span className="value">{Math.round(filter.min)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="control-row">
|
|
||||||
<label>Max:</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className="range-control"
|
|
||||||
value={filter.max}
|
|
||||||
min={filter.min}
|
|
||||||
max={200}
|
|
||||||
onChange={(e) => handleFilterChange('max', parseFloat(e.target.value))}
|
|
||||||
/>
|
|
||||||
<span className="value">{Math.round(filter.max)}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="control-row">
|
|
||||||
<label>Frequency:</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
className="range-control"
|
|
||||||
value={filter.frequency * 100}
|
|
||||||
min={1}
|
|
||||||
max={200}
|
|
||||||
onChange={(e) => handleFilterChange('frequency', parseFloat(e.target.value) / 100)}
|
|
||||||
/>
|
|
||||||
<span className="value">{filter.frequency.toFixed(2)}Hz</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="button button-reset"
|
|
||||||
style={{ marginTop: '5px', fontSize: '11px', padding: '3px 8px' }}
|
|
||||||
onClick={() => removeFilter(property, index)}
|
|
||||||
>
|
|
||||||
Remove Filter
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ControlFilter;
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { HexColorPicker } from 'react-colorful';
|
|
||||||
import useAnimationStore from '../store/animationStore';
|
|
||||||
import ControlFilter from './ControlFilter';
|
|
||||||
|
|
||||||
const Toolbar = ({ isVisible }) => {
|
|
||||||
const {
|
|
||||||
selectedAnimation,
|
|
||||||
setSelectedAnimation,
|
|
||||||
animations,
|
|
||||||
updateParameter,
|
|
||||||
addFilter,
|
|
||||||
paused,
|
|
||||||
togglePause,
|
|
||||||
resetAnimation,
|
|
||||||
speedMultiplier,
|
|
||||||
} = useAnimationStore();
|
|
||||||
|
|
||||||
const currentAnimation = animations[selectedAnimation];
|
|
||||||
|
|
||||||
const handleAnimationChange = (e) => {
|
|
||||||
setSelectedAnimation(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleParameterChange = (property, value) => {
|
|
||||||
updateParameter(property, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderControl = (control) => {
|
|
||||||
const value = currentAnimation.parameters[control.property];
|
|
||||||
|
|
||||||
switch (control.type) {
|
|
||||||
case 'range':
|
|
||||||
return (
|
|
||||||
<div className="control-row" key={control.property}>
|
|
||||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
|
||||||
<input
|
|
||||||
id={`control-${control.property}`}
|
|
||||||
type="range"
|
|
||||||
min={control.min}
|
|
||||||
max={control.max}
|
|
||||||
step={1}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => handleParameterChange(control.property, parseFloat(e.target.value))}
|
|
||||||
className="range-control"
|
|
||||||
/>
|
|
||||||
<span className="value">{Math.round(value * 100) / 100}</span>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="add-filter-button"
|
|
||||||
onClick={() => addFilter(control.property)}
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'color':
|
|
||||||
return (
|
|
||||||
<div className="control-container" key={control.property}>
|
|
||||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
|
||||||
<div style={{ margin: '10px 0' }}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '20px',
|
|
||||||
background: value,
|
|
||||||
marginBottom: '5px',
|
|
||||||
border: '1px solid #444'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<HexColorPicker
|
|
||||||
color={value}
|
|
||||||
onChange={(color) => handleParameterChange(control.property, color)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
case 'checkbox':
|
|
||||||
return (
|
|
||||||
<div className="control-row" key={control.property}>
|
|
||||||
<label htmlFor={`control-${control.property}`}>{control.label || control.property}:</label>
|
|
||||||
<input
|
|
||||||
id={`control-${control.property}`}
|
|
||||||
type="checkbox"
|
|
||||||
checked={value}
|
|
||||||
onChange={(e) => handleParameterChange(control.property, e.target.checked)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFilters = (property) => {
|
|
||||||
const filters = currentAnimation.filters[property];
|
|
||||||
if (!filters || filters.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="filter-container">
|
|
||||||
<div className="filter-header">Filters</div>
|
|
||||||
{filters.map((filter, index) => (
|
|
||||||
<ControlFilter
|
|
||||||
key={`${property}-filter-${index}`}
|
|
||||||
filter={filter}
|
|
||||||
property={property}
|
|
||||||
index={index}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`toolbar ${isVisible ? '' : 'hidden'}`}>
|
|
||||||
<h2>Animation Controls</h2>
|
|
||||||
|
|
||||||
<div className="control-container">
|
|
||||||
<label htmlFor="animation-selector">Animation:</label>
|
|
||||||
<select
|
|
||||||
id="animation-selector"
|
|
||||||
value={selectedAnimation}
|
|
||||||
onChange={handleAnimationChange}
|
|
||||||
className="shape-selector"
|
|
||||||
>
|
|
||||||
{Object.keys(animations).map(animName => (
|
|
||||||
<option key={animName} value={animName}>{animName}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="control-container">
|
|
||||||
<button
|
|
||||||
className="button"
|
|
||||||
onClick={togglePause}
|
|
||||||
>
|
|
||||||
{paused ? 'Play' : 'Pause'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="button button-reset"
|
|
||||||
onClick={resetAnimation}
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="control-row">
|
|
||||||
<label htmlFor="speed-multiplier">Speed:</label>
|
|
||||||
<input
|
|
||||||
id="speed-multiplier"
|
|
||||||
type="range"
|
|
||||||
min={1}
|
|
||||||
max={500}
|
|
||||||
value={speedMultiplier}
|
|
||||||
onChange={(e) => useAnimationStore.setState({ speedMultiplier: parseInt(e.target.value) })}
|
|
||||||
className="range-control"
|
|
||||||
/>
|
|
||||||
<span className="value">{speedMultiplier}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr style={{ margin: '20px 0' }} />
|
|
||||||
|
|
||||||
<h3>Parameters</h3>
|
|
||||||
{currentAnimation.config.map(control => (
|
|
||||||
<React.Fragment key={control.property}>
|
|
||||||
{renderControl(control)}
|
|
||||||
{renderFilters(control.property)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Toolbar;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
export const FloralPhyllo = ({ scene, rotation, params, viewport, objectsToDispose = [] }) => {
|
|
||||||
const {
|
|
||||||
width,
|
|
||||||
depth,
|
|
||||||
start,
|
|
||||||
colour1,
|
|
||||||
colour2
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
// The effective rotation with start offset
|
|
||||||
const effectiveRotation = rotation + start;
|
|
||||||
|
|
||||||
// Generate points in phyllotaxis pattern
|
|
||||||
for (let n = 1; n <= depth; n++) {
|
|
||||||
// Calculate position using phyllotaxis formula
|
|
||||||
// Use the golden angle (137.5 degrees) for natural-looking pattern
|
|
||||||
const a = n * 0.1 + effectiveRotation / 100;
|
|
||||||
const r = Math.sqrt(n) * (width / 25);
|
|
||||||
const x = r * Math.cos(a);
|
|
||||||
const y = r * Math.sin(a);
|
|
||||||
|
|
||||||
// Calculate color using gradient based on position in the sequence
|
|
||||||
const colorFraction = n / depth;
|
|
||||||
const color = lerpColor(colour1, colour2, colorFraction);
|
|
||||||
|
|
||||||
// Create a petal/eye shape at this position
|
|
||||||
createPetal(scene, n/2 + 10, x, y, 0, color, objectsToDispose);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to create a petal/eye shape
|
|
||||||
function createPetal(scene, size, x, y, z, color, objectsToDispose) {
|
|
||||||
// Create a custom shape for the petal/eye
|
|
||||||
const shape = new THREE.Shape();
|
|
||||||
|
|
||||||
// Define control points for the petal shape
|
|
||||||
const halfSize = size / 2;
|
|
||||||
|
|
||||||
// Starting point
|
|
||||||
shape.moveTo(-halfSize, 0);
|
|
||||||
|
|
||||||
// Top curve
|
|
||||||
shape.quadraticCurveTo(0, halfSize, halfSize, 0);
|
|
||||||
|
|
||||||
// Bottom curve
|
|
||||||
shape.quadraticCurveTo(0, -halfSize, -halfSize, 0);
|
|
||||||
|
|
||||||
// Create geometry from shape
|
|
||||||
const geometry = new THREE.ShapeGeometry(shape);
|
|
||||||
|
|
||||||
// Create material with specified color
|
|
||||||
const material = new THREE.MeshBasicMaterial({
|
|
||||||
color: new THREE.Color(color),
|
|
||||||
side: THREE.DoubleSide
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create mesh and position it
|
|
||||||
const petal = new THREE.Mesh(geometry, material);
|
|
||||||
petal.position.set(x, y, z);
|
|
||||||
|
|
||||||
// Calculate angle based on position relative to origin
|
|
||||||
const angle = Math.atan2(y, x);
|
|
||||||
petal.rotation.z = angle; // Rotate to face outward
|
|
||||||
|
|
||||||
// Add stroke outline
|
|
||||||
const edgesGeometry = new THREE.EdgesGeometry(geometry);
|
|
||||||
const edgesMaterial = new THREE.LineBasicMaterial({
|
|
||||||
color: new THREE.Color(0x000000),
|
|
||||||
linewidth: 1
|
|
||||||
});
|
|
||||||
const edges = new THREE.LineSegments(edgesGeometry, edgesMaterial);
|
|
||||||
|
|
||||||
petal.add(edges);
|
|
||||||
scene.add(petal);
|
|
||||||
|
|
||||||
// Add to objects to dispose list
|
|
||||||
if (objectsToDispose) {
|
|
||||||
objectsToDispose.push(petal);
|
|
||||||
// Also track the edges for disposal
|
|
||||||
objectsToDispose.push(edges);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert hex color string to THREE.js color and interpolate
|
|
||||||
function lerpColor(colorA, colorB, t) {
|
|
||||||
const a = new THREE.Color(colorA);
|
|
||||||
const b = new THREE.Color(colorB);
|
|
||||||
|
|
||||||
return new THREE.Color(
|
|
||||||
a.r + (b.r - a.r) * t,
|
|
||||||
a.g + (b.g - a.g) * t,
|
|
||||||
a.b + (b.b - a.b) * t
|
|
||||||
).getHex();
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
export const PolyTwistColourWidth = ({ scene, rotation, params, viewport, objectsToDispose = [] }) => {
|
|
||||||
const {
|
|
||||||
sides,
|
|
||||||
width,
|
|
||||||
lineWidth,
|
|
||||||
depth,
|
|
||||||
rotation: rotationParam,
|
|
||||||
colour1,
|
|
||||||
colour2
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
// Calculate the inner angle of the polygon
|
|
||||||
const innerAngle = 180 - ((sides - 2) * 180) / sides;
|
|
||||||
const scopeAngle = rotation - (innerAngle * Math.floor(rotation / innerAngle));
|
|
||||||
|
|
||||||
// Calculate the twist angle
|
|
||||||
let outAngle = 0;
|
|
||||||
if (scopeAngle < innerAngle / 2) {
|
|
||||||
outAngle = innerAngle / (2 * Math.cos((2 * Math.PI * scopeAngle) / (3 * innerAngle))) - innerAngle / 2;
|
|
||||||
} else {
|
|
||||||
outAngle = -innerAngle / (2 * Math.cos(((2 * Math.PI) / 3) - ((2 * Math.PI * scopeAngle) / (3 * innerAngle)))) + (innerAngle * 3) / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate width multiplier
|
|
||||||
const minWidth = Math.sin(toRadians(innerAngle / 2)) * (0.5 / Math.tan(toRadians(innerAngle / 2))) * 2;
|
|
||||||
const widthMultiplier = minWidth / Math.sin(Math.PI / 180 * (90 + innerAngle / 2 - outAngle + innerAngle * Math.floor(outAngle / innerAngle)));
|
|
||||||
|
|
||||||
// Draw each polygon with increasing size and color transition
|
|
||||||
for (let i = 0; i < depth; i++) {
|
|
||||||
const fraction = i / depth;
|
|
||||||
const color = lerpColor(colour1, colour2, fraction);
|
|
||||||
|
|
||||||
// Create a polygon shape
|
|
||||||
drawPolygon(
|
|
||||||
scene,
|
|
||||||
sides,
|
|
||||||
width * Math.pow(widthMultiplier, i),
|
|
||||||
outAngle * i + rotationParam,
|
|
||||||
color,
|
|
||||||
lineWidth,
|
|
||||||
objectsToDispose
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper functions
|
|
||||||
function drawPolygon(scene, sides, width, rotation, color, lineWidth, objectsToDispose) {
|
|
||||||
const points = [];
|
|
||||||
|
|
||||||
// Create points for the polygon
|
|
||||||
for (let i = 0; i <= sides; i++) {
|
|
||||||
const angle = (i * 2 * Math.PI) / sides + (rotation * Math.PI) / 180;
|
|
||||||
points.push(new THREE.Vector3(
|
|
||||||
width * Math.cos(angle),
|
|
||||||
width * Math.sin(angle),
|
|
||||||
0
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create geometry from points
|
|
||||||
const geometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
||||||
|
|
||||||
// Create material with specified color
|
|
||||||
const material = new THREE.LineBasicMaterial({
|
|
||||||
color: new THREE.Color("#" + color),
|
|
||||||
linewidth: lineWidth // Note: linewidth may not work as expected in WebGL
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and add the line
|
|
||||||
const line = new THREE.Line(geometry, material);
|
|
||||||
scene.add(line);
|
|
||||||
|
|
||||||
// Add to objects to dispose list
|
|
||||||
if (objectsToDispose) {
|
|
||||||
objectsToDispose.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert hex color string to THREE.js color
|
|
||||||
function lerpColor(colorA, colorB, t) {
|
|
||||||
const a = new THREE.Color(colorA);
|
|
||||||
const b = new THREE.Color(colorB);
|
|
||||||
|
|
||||||
return new THREE.Color(
|
|
||||||
a.r + (b.r - a.r) * t,
|
|
||||||
a.g + (b.g - a.g) * t,
|
|
||||||
a.b + (b.b - a.b) * t
|
|
||||||
).getHexString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert degrees to radians
|
|
||||||
function toRadians(degrees) {
|
|
||||||
return degrees * Math.PI / 180;
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
export const RaysInShape = ({ scene, elapsedTime, deltaTime, params, viewport, objectsToDispose = [] }) => {
|
|
||||||
const {
|
|
||||||
rays,
|
|
||||||
speed,
|
|
||||||
doesWave,
|
|
||||||
speedVertRate,
|
|
||||||
speedHorrRate,
|
|
||||||
speedVert,
|
|
||||||
speedHorr,
|
|
||||||
boxSize,
|
|
||||||
trailLength,
|
|
||||||
lineWidth,
|
|
||||||
fade,
|
|
||||||
colourFree,
|
|
||||||
colourContained,
|
|
||||||
boxVisible
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
// Define box boundaries
|
|
||||||
const halfBoxSize = boxSize / 200;
|
|
||||||
const boxLeft = -halfBoxSize;
|
|
||||||
const boxRight = halfBoxSize;
|
|
||||||
const boxTop = -halfBoxSize;
|
|
||||||
const boxBottom = halfBoxSize;
|
|
||||||
|
|
||||||
// Draw the box if visible
|
|
||||||
if (boxVisible) {
|
|
||||||
const boxGeometry = new THREE.BufferGeometry();
|
|
||||||
const boxVertices = new Float32Array([
|
|
||||||
boxLeft, boxTop, 0,
|
|
||||||
boxRight, boxTop, 0,
|
|
||||||
boxRight, boxBottom, 0,
|
|
||||||
boxLeft, boxBottom, 0,
|
|
||||||
boxLeft, boxTop, 0
|
|
||||||
]);
|
|
||||||
boxGeometry.setAttribute('position', new THREE.BufferAttribute(boxVertices, 3));
|
|
||||||
const boxMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
|
|
||||||
const box = new THREE.Line(boxGeometry, boxMaterial);
|
|
||||||
scene.add(box);
|
|
||||||
|
|
||||||
// Track box for disposal
|
|
||||||
if (objectsToDispose) {
|
|
||||||
objectsToDispose.push(box);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate current wave values if waves are enabled
|
|
||||||
let currentSpeedVert = speedVert;
|
|
||||||
let currentSpeedHorr = speedHorr;
|
|
||||||
|
|
||||||
if (doesWave) {
|
|
||||||
const vertRate = speedVertRate / 1000;
|
|
||||||
const horrRate = speedHorrRate / 1000;
|
|
||||||
currentSpeedVert = Math.sin(elapsedTime * vertRate) * 85 + 100;
|
|
||||||
currentSpeedHorr = Math.sin(elapsedTime * horrRate) * 85 + 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate rays
|
|
||||||
const actualRayCount = Math.min(rays, 100); // Limit for performance
|
|
||||||
for (let i = 0; i < actualRayCount; i++) {
|
|
||||||
const angle = (i * 360) / actualRayCount;
|
|
||||||
|
|
||||||
// Create ray trajectory
|
|
||||||
const positions = generateRayPositions(
|
|
||||||
0, 0, 0, // Start at center
|
|
||||||
angle,
|
|
||||||
speed / 10000,
|
|
||||||
currentSpeedVert / 100,
|
|
||||||
currentSpeedHorr / 100,
|
|
||||||
boxLeft,
|
|
||||||
boxRight,
|
|
||||||
boxTop,
|
|
||||||
boxBottom,
|
|
||||||
elapsedTime
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw the ray trail
|
|
||||||
drawRayTrail(
|
|
||||||
scene,
|
|
||||||
positions,
|
|
||||||
Math.min(trailLength, 30), // Limit trail length for performance
|
|
||||||
lineWidth / 10,
|
|
||||||
colourContained,
|
|
||||||
fade,
|
|
||||||
objectsToDispose
|
|
||||||
);
|
|
||||||
|
|
||||||
// Draw center-bound rays from collision points
|
|
||||||
positions.forEach(pos => {
|
|
||||||
if (pos.collision) {
|
|
||||||
const angleToCenter = Math.atan2(-pos.y, -pos.x) * 180 / Math.PI;
|
|
||||||
|
|
||||||
const centerTrail = generateCenterRayPositions(
|
|
||||||
pos.x, pos.y, 0,
|
|
||||||
angleToCenter,
|
|
||||||
speed / 8000,
|
|
||||||
Math.min(trailLength / 2, 15) // Limit trail length for performance
|
|
||||||
);
|
|
||||||
|
|
||||||
drawRayTrail(
|
|
||||||
scene,
|
|
||||||
centerTrail,
|
|
||||||
Math.min(trailLength / 2, 15),
|
|
||||||
lineWidth / 10,
|
|
||||||
colourFree,
|
|
||||||
fade,
|
|
||||||
objectsToDispose
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate positions for a ray
|
|
||||||
function generateRayPositions(
|
|
||||||
startX, startY, startZ,
|
|
||||||
angle,
|
|
||||||
speed,
|
|
||||||
speedVert,
|
|
||||||
speedHorr,
|
|
||||||
boxLeft,
|
|
||||||
boxRight,
|
|
||||||
boxTop,
|
|
||||||
boxBottom,
|
|
||||||
elapsedTime
|
|
||||||
) {
|
|
||||||
const positions = [];
|
|
||||||
let x = startX;
|
|
||||||
let y = startY;
|
|
||||||
let z = startZ;
|
|
||||||
let currentAngle = angle;
|
|
||||||
let collisionCount = 0;
|
|
||||||
const maxCollisions = 5; // Limit collisions for performance
|
|
||||||
|
|
||||||
// Calculate initial direction
|
|
||||||
const radians = (currentAngle * Math.PI) / 180;
|
|
||||||
let dirX = Math.cos(radians);
|
|
||||||
let dirY = Math.sin(radians);
|
|
||||||
|
|
||||||
// Add starting position
|
|
||||||
positions.push({ x, y, z, angle: currentAngle });
|
|
||||||
|
|
||||||
// Move ray until collision or max collisions reached
|
|
||||||
while (collisionCount < maxCollisions) {
|
|
||||||
// Calculate new position
|
|
||||||
const dx = speedHorr * speed * dirX;
|
|
||||||
const dy = speedVert * speed * dirY;
|
|
||||||
|
|
||||||
x += dx;
|
|
||||||
y += dy;
|
|
||||||
|
|
||||||
// Check for collisions
|
|
||||||
let collision = null;
|
|
||||||
|
|
||||||
// Check horizontal boundaries
|
|
||||||
if (x < boxLeft) {
|
|
||||||
x = boxLeft;
|
|
||||||
currentAngle = 180 - currentAngle;
|
|
||||||
collision = 'left';
|
|
||||||
} else if (x > boxRight) {
|
|
||||||
x = boxRight;
|
|
||||||
currentAngle = 180 - currentAngle;
|
|
||||||
collision = 'right';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check vertical boundaries
|
|
||||||
if (y < boxTop) {
|
|
||||||
y = boxTop;
|
|
||||||
currentAngle = 360 - currentAngle;
|
|
||||||
collision = 'top';
|
|
||||||
} else if (y > boxBottom) {
|
|
||||||
y = boxBottom;
|
|
||||||
currentAngle = 360 - currentAngle;
|
|
||||||
collision = 'bottom';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize angle
|
|
||||||
currentAngle = ((currentAngle % 360) + 360) % 360;
|
|
||||||
|
|
||||||
// Add position to array
|
|
||||||
positions.push({
|
|
||||||
x, y, z,
|
|
||||||
angle: currentAngle,
|
|
||||||
collision
|
|
||||||
});
|
|
||||||
|
|
||||||
// If collision occurred, update direction and increment counter
|
|
||||||
if (collision) {
|
|
||||||
const newRadians = (currentAngle * Math.PI) / 180;
|
|
||||||
dirX = Math.cos(newRadians);
|
|
||||||
dirY = Math.sin(newRadians);
|
|
||||||
collisionCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break if we're outside the box
|
|
||||||
if (x < boxLeft * 2 || x > boxRight * 2 || y < boxTop * 2 || y > boxBottom * 2) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate center-bound ray positions
|
|
||||||
function generateCenterRayPositions(startX, startY, startZ, angle, speed, trailLength) {
|
|
||||||
const positions = [];
|
|
||||||
let x = startX;
|
|
||||||
let y = startY;
|
|
||||||
let z = startZ;
|
|
||||||
|
|
||||||
// Calculate direction toward center
|
|
||||||
const radians = (angle * Math.PI) / 180;
|
|
||||||
const dirX = Math.cos(radians);
|
|
||||||
const dirY = Math.sin(radians);
|
|
||||||
|
|
||||||
// Add starting position
|
|
||||||
positions.push({ x, y, z, angle });
|
|
||||||
|
|
||||||
// Generate trail points
|
|
||||||
for (let i = 0; i < trailLength; i++) {
|
|
||||||
// Move toward center
|
|
||||||
x += dirX * speed;
|
|
||||||
y += dirY * speed;
|
|
||||||
|
|
||||||
// Add to positions
|
|
||||||
positions.push({ x, y, z, angle });
|
|
||||||
|
|
||||||
// If very close to center, stop
|
|
||||||
if (Math.abs(x) < 0.01 && Math.abs(y) < 0.01) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return positions;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw a ray trail
|
|
||||||
function drawRayTrail(scene, positions, maxPoints, lineWidth, color, fade, objectsToDispose) {
|
|
||||||
// Use only a portion of positions for performance
|
|
||||||
const usePositions = positions.slice(0, maxPoints);
|
|
||||||
|
|
||||||
// Create line segments for the ray
|
|
||||||
for (let i = 1; i < usePositions.length; i++) {
|
|
||||||
const prev = usePositions[i - 1];
|
|
||||||
const curr = usePositions[i];
|
|
||||||
|
|
||||||
// Calculate opacity based on position in trail if fade is enabled
|
|
||||||
let opacity = 1;
|
|
||||||
if (fade) {
|
|
||||||
opacity = 1 - (i / usePositions.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create geometry for line segment
|
|
||||||
const geometry = new THREE.BufferGeometry();
|
|
||||||
const vertices = new Float32Array([
|
|
||||||
prev.x, prev.y, prev.z,
|
|
||||||
curr.x, curr.y, curr.z
|
|
||||||
]);
|
|
||||||
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
|
|
||||||
|
|
||||||
// Create material with appropriate color
|
|
||||||
const material = new THREE.LineBasicMaterial({
|
|
||||||
color: curr.collision ? 0xffff00 : new THREE.Color(color),
|
|
||||||
transparent: fade,
|
|
||||||
opacity: opacity,
|
|
||||||
linewidth: lineWidth
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create and add the line
|
|
||||||
const line = new THREE.Line(geometry, material);
|
|
||||||
scene.add(line);
|
|
||||||
|
|
||||||
// Track line for disposal
|
|
||||||
if (objectsToDispose) {
|
|
||||||
objectsToDispose.push(line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
background-color: black;
|
|
||||||
color: white;
|
|
||||||
overflow: hidden;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.App {
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas {
|
|
||||||
display: block;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
touch-action: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 300px;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(32, 32, 32, 0.8);
|
|
||||||
padding: 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: Roboto, system-ui;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toolbar.hidden {
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-container {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-label {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shape-selector {
|
|
||||||
width: 100%;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button {
|
|
||||||
display: inline-block;
|
|
||||||
background-color: #e1ecf4;
|
|
||||||
border-radius: 3px;
|
|
||||||
border: 1px solid #7aa7c7;
|
|
||||||
box-sizing: border-box;
|
|
||||||
color: #1f3f55;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 8px 12px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 5px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover {
|
|
||||||
background-color: #b3d3ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-reset {
|
|
||||||
background-color: #f4e1e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 320px;
|
|
||||||
z-index: 11;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: rgba(32, 32, 32, 0.8);
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-container {
|
|
||||||
margin-left: 20px;
|
|
||||||
padding: 10px;
|
|
||||||
border-left: 1px solid #444;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-header {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-control {
|
|
||||||
width: 100%;
|
|
||||||
background: #444;
|
|
||||||
height: 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
outline: none;
|
|
||||||
transition: background 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.range-control::-webkit-slider-thumb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
appearance: none;
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #e1ecf4;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-filter-button {
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 3px 8px;
|
|
||||||
background-color: #444;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
border-radius: 3px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-filter-button:hover {
|
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row label {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row input[type="range"] {
|
|
||||||
flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-row .value {
|
|
||||||
flex: 0 0 40px;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import './index.css';
|
|
||||||
import App from './App';
|
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
|
||||||
const root = createRoot(container);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<App />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
import { create } from 'zustand';
|
|
||||||
|
|
||||||
// Animation definitions with their configurations
|
|
||||||
const animationConfigs = {
|
|
||||||
PolyTwistColourWidth: [
|
|
||||||
{ type: "range", min: 3, max: 10, defaultValue: 5, property: "sides", label: "Sides" },
|
|
||||||
{ type: "range", min: 400, max: 2000, defaultValue: 400, property: "width", label: "Width" },
|
|
||||||
{ type: "range", min: 2, max: 5, defaultValue: 5, property: "lineWidth", label: "Line Width" },
|
|
||||||
{ type: "range", min: 1, max: 100, defaultValue: 50, property: "depth", label: "Depth" },
|
|
||||||
{ type: "range", min: -180, max: 180, defaultValue: -90, property: "rotation", label: "Rotation" },
|
|
||||||
{ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier", label: "Speed" },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour1", label: "Color 1" },
|
|
||||||
{ type: "color", defaultValue: "#42f57b", property: "colour2", label: "Color 2" },
|
|
||||||
],
|
|
||||||
FloralPhyllo: [
|
|
||||||
{ type: "range", min: 1, max: 600, defaultValue: 300, property: "width", label: "Width" },
|
|
||||||
{ type: "range", min: 1, max: 300, defaultValue: 150, property: "depth", label: "Depth" },
|
|
||||||
{ type: "range", min: 0, max: 3141, defaultValue: 0, property: "start", label: "Start" },
|
|
||||||
{ type: "color", defaultValue: "#4287f5", property: "colour1", label: "Color 1" },
|
|
||||||
{ type: "color", defaultValue: "#FC0362", property: "colour2", label: "Color 2" },
|
|
||||||
],
|
|
||||||
RaysInShape: [
|
|
||||||
{ type: "range", min: 50, max: 1000, defaultValue: 500, property: "rays", label: "Rays" },
|
|
||||||
{ type: "range", min: 1, max: 30, defaultValue: 2, property: "speed", label: "Speed" },
|
|
||||||
{ type: "checkbox", defaultValue: true, property: "doesWave", label: "Wave Effect" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVertRate", label: "Vertical Rate" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorrRate", label: "Horizontal Rate" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedVert", label: "Vertical Speed" },
|
|
||||||
{ type: "range", min: 1, max: 200, defaultValue: 100, property: "speedHorr", label: "Horizontal Speed" },
|
|
||||||
{ type: "range", min: 10, max: 2000, defaultValue: 800, property: "boxSize", label: "Box Size" },
|
|
||||||
{ type: "range", min: 1, max: 80, defaultValue: 5, property: "trailLength", label: "Trail Length" },
|
|
||||||
{ type: "range", min: 1, max: 500, defaultValue: 5, property: "lineWidth", label: "Line Width" },
|
|
||||||
{ type: "checkbox", defaultValue: false, property: "fade", label: "Fade Effect" },
|
|
||||||
{ type: "color", defaultValue: "#43dbad", property: "colourFree", label: "Free Color" },
|
|
||||||
{ type: "color", defaultValue: "#f05c79", property: "colourContained", label: "Contained Color" },
|
|
||||||
{ type: "checkbox", defaultValue: false, property: "boxVisible", label: "Show Box" },
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate default parameters for each animation
|
|
||||||
const generateDefaultParameters = (config) => {
|
|
||||||
const defaults = {};
|
|
||||||
config.forEach(item => {
|
|
||||||
defaults[item.property] = item.defaultValue;
|
|
||||||
});
|
|
||||||
return defaults;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper function to create an animation state object
|
|
||||||
const createAnimationState = () => {
|
|
||||||
const animations = {};
|
|
||||||
|
|
||||||
Object.keys(animationConfigs).forEach(animName => {
|
|
||||||
animations[animName] = {
|
|
||||||
config: animationConfigs[animName],
|
|
||||||
parameters: generateDefaultParameters(animationConfigs[animName]),
|
|
||||||
baseParameters: generateDefaultParameters(animationConfigs[animName]), // Store original values
|
|
||||||
filters: {} // Will store filters applied to parameters
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return animations;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create the store
|
|
||||||
const useAnimationStore = create((set, get) => ({
|
|
||||||
// Store all animation configurations and their parameters
|
|
||||||
animations: createAnimationState(),
|
|
||||||
|
|
||||||
// Currently selected animation
|
|
||||||
selectedAnimation: 'PolyTwistColourWidth',
|
|
||||||
|
|
||||||
// Animation control
|
|
||||||
paused: false,
|
|
||||||
elapsedTime: 0,
|
|
||||||
rotation: 0,
|
|
||||||
speedMultiplier: 100,
|
|
||||||
|
|
||||||
// Set the selected animation
|
|
||||||
setSelectedAnimation: (animationName) => {
|
|
||||||
set({ selectedAnimation: animationName });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Toggle pause state
|
|
||||||
togglePause: () => {
|
|
||||||
set(state => ({ paused: !state.paused }));
|
|
||||||
},
|
|
||||||
|
|
||||||
// Reset the animation
|
|
||||||
resetAnimation: () => {
|
|
||||||
set({ rotation: 0, elapsedTime: 0 });
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update a parameter for the current animation
|
|
||||||
updateParameter: (property, value) => {
|
|
||||||
set(state => {
|
|
||||||
const animName = state.selectedAnimation;
|
|
||||||
return {
|
|
||||||
animations: {
|
|
||||||
...state.animations,
|
|
||||||
[animName]: {
|
|
||||||
...state.animations[animName],
|
|
||||||
parameters: {
|
|
||||||
...state.animations[animName].parameters,
|
|
||||||
[property]: value
|
|
||||||
},
|
|
||||||
baseParameters: {
|
|
||||||
...state.animations[animName].baseParameters,
|
|
||||||
[property]: value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Add a filter to a parameter
|
|
||||||
addFilter: (property, filterType = 'sine') => {
|
|
||||||
set(state => {
|
|
||||||
const animName = state.selectedAnimation;
|
|
||||||
const config = state.animations[animName].config.find(c => c.property === property);
|
|
||||||
|
|
||||||
if (!config) return state;
|
|
||||||
|
|
||||||
// Calculate default min and max based on the base parameter value
|
|
||||||
const baseValue = state.animations[animName].baseParameters[property];
|
|
||||||
const range = config.max - config.min;
|
|
||||||
const offset = range * 0.2; // 20% of range
|
|
||||||
|
|
||||||
// Create default filter with min/max values
|
|
||||||
const newFilter = {
|
|
||||||
type: filterType,
|
|
||||||
min: baseValue - offset,
|
|
||||||
max: baseValue + offset,
|
|
||||||
frequency: 0.3, // Hz - cycles per second
|
|
||||||
phase: Math.random() * Math.PI * 2, // Random phase offset between 0 and 2π
|
|
||||||
enabled: true
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentFilters = state.animations[animName].filters[property] || [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
animations: {
|
|
||||||
...state.animations,
|
|
||||||
[animName]: {
|
|
||||||
...state.animations[animName],
|
|
||||||
filters: {
|
|
||||||
...state.animations[animName].filters,
|
|
||||||
[property]: [...currentFilters, newFilter]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update a filter
|
|
||||||
updateFilter: (property, filterIndex, filterProperty, value) => {
|
|
||||||
set(state => {
|
|
||||||
const animName = state.selectedAnimation;
|
|
||||||
const filters = state.animations[animName].filters[property] || [];
|
|
||||||
|
|
||||||
if (filterIndex >= filters.length) return state;
|
|
||||||
|
|
||||||
const updatedFilters = [...filters];
|
|
||||||
updatedFilters[filterIndex] = {
|
|
||||||
...updatedFilters[filterIndex],
|
|
||||||
[filterProperty]: value
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
animations: {
|
|
||||||
...state.animations,
|
|
||||||
[animName]: {
|
|
||||||
...state.animations[animName],
|
|
||||||
filters: {
|
|
||||||
...state.animations[animName].filters,
|
|
||||||
[property]: updatedFilters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Remove a filter
|
|
||||||
removeFilter: (property, filterIndex) => {
|
|
||||||
set(state => {
|
|
||||||
const animName = state.selectedAnimation;
|
|
||||||
const filters = state.animations[animName].filters[property] || [];
|
|
||||||
|
|
||||||
if (filterIndex >= filters.length) return state;
|
|
||||||
|
|
||||||
const updatedFilters = filters.filter((_, i) => i !== filterIndex);
|
|
||||||
|
|
||||||
return {
|
|
||||||
animations: {
|
|
||||||
...state.animations,
|
|
||||||
[animName]: {
|
|
||||||
...state.animations[animName],
|
|
||||||
filters: {
|
|
||||||
...state.animations[animName].filters,
|
|
||||||
[property]: updatedFilters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// Update the animation state on each frame
|
|
||||||
updateAnimation: (deltaTime) => {
|
|
||||||
set(state => {
|
|
||||||
if (state.paused) return state;
|
|
||||||
|
|
||||||
const newElapsedTime = state.elapsedTime + deltaTime;
|
|
||||||
const newRotation = state.rotation + (deltaTime * state.speedMultiplier / 100);
|
|
||||||
|
|
||||||
// Apply filters to parameters
|
|
||||||
const animName = state.selectedAnimation;
|
|
||||||
const animation = state.animations[animName];
|
|
||||||
const updatedParameters = { ...animation.parameters };
|
|
||||||
|
|
||||||
// Process each parameter that has filters
|
|
||||||
Object.entries(animation.filters).forEach(([property, filters]) => {
|
|
||||||
if (!filters || filters.length === 0) return;
|
|
||||||
|
|
||||||
// Start with the base parameter value (unmodified by filters)
|
|
||||||
const baseValue = animation.baseParameters[property];
|
|
||||||
|
|
||||||
// Find the config for this property to get min/max bounds
|
|
||||||
const config = animation.config.find(c => c.property === property);
|
|
||||||
const propMin = config?.min;
|
|
||||||
const propMax = config?.max;
|
|
||||||
|
|
||||||
// Calculate the combined effect of all filters
|
|
||||||
let totalModification = 0;
|
|
||||||
|
|
||||||
// Apply each filter to build up the modifications
|
|
||||||
filters.forEach(filter => {
|
|
||||||
if (!filter.enabled) return;
|
|
||||||
|
|
||||||
if (filter.type === 'sine') {
|
|
||||||
// Calculate sine wave value based on time and filter properties
|
|
||||||
const frequency = filter.frequency || 0.3; // Hz
|
|
||||||
const phase = filter.phase || 0;
|
|
||||||
const min = filter.min || baseValue - 10;
|
|
||||||
const max = filter.max || baseValue + 10;
|
|
||||||
|
|
||||||
// Calculate center and amplitude from min/max
|
|
||||||
const center = (min + max) / 2;
|
|
||||||
const amplitude = (max - min) / 2;
|
|
||||||
|
|
||||||
// Create sinusoidal oscillation between min and max values
|
|
||||||
// const sineValue = center + Math.sin(newElapsedTime * 2 * Math.PI * frequency + phase) * amplitude;
|
|
||||||
const sineValue = min + amplitude + Math.sin(newElapsedTime * (1 / frequency)) * amplitude;
|
|
||||||
|
|
||||||
// Instead of direct modification, calculate the difference from base
|
|
||||||
const modification = sineValue;
|
|
||||||
|
|
||||||
console.log(`Filter: ${property}, Sine Value: ${sineValue}, Modification: ${modification}`);
|
|
||||||
// Add this filter's contribution to the total
|
|
||||||
totalModification += modification;
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply total modification to base value
|
|
||||||
// let filteredValue = baseValue + totalModification;
|
|
||||||
let filteredValue = totalModification;
|
|
||||||
|
|
||||||
// Constrain to the property's min/max if available
|
|
||||||
if (propMin !== undefined && filteredValue < propMin) filteredValue = propMin;
|
|
||||||
if (propMax !== undefined && filteredValue > propMax) filteredValue = propMax;
|
|
||||||
|
|
||||||
// Update the parameter with the filtered value
|
|
||||||
updatedParameters[property] = filteredValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
elapsedTime: newElapsedTime,
|
|
||||||
rotation: newRotation,
|
|
||||||
animations: {
|
|
||||||
...state.animations,
|
|
||||||
[animName]: {
|
|
||||||
...animation,
|
|
||||||
parameters: updatedParameters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default useAnimationStore;
|
|
||||||
Reference in New Issue
Block a user