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;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
|
||||
position: absolute;
|
||||
padding: 0px 20px 0px 20px;
|
||||
width: 500px;
|
||||
height: 100vh;
|
||||
background-color: rgba(32, 32, 32, 0.616);
|
||||
overflow-y: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
#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{
|
||||
color: #e0e0e0;
|
||||
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 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
@@ -149,4 +117,820 @@ canvas {
|
||||
|
||||
.buttonReset{
|
||||
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;">
|
||||
<canvas id="myCanvas" style="display: block;box-sizing: border-box;"></canvas>
|
||||
<div id="toolbar">
|
||||
<br>
|
||||
<select id="shape-selector">
|
||||
<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>
|
||||
<!-- Shape layers -->
|
||||
<div id="layers-container"></div>
|
||||
<br>
|
||||
<p>Controls:</p>
|
||||
<p>
|
||||
@@ -52,10 +36,40 @@
|
||||
<button onclick="manualToggleSettings()" class="button">Show/hide</button>
|
||||
</div>
|
||||
</body>
|
||||
<script src="./js/helper.js" type="text/javascript"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<!-- Utilities (must load first - used by everything) -->
|
||||
<script src="./js/utils/helpers.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>
|
||||
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
|
||||
|
||||
/**
|
||||
* 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 ctx = c.getContext("2d");
|
||||
ctx.canvas.width = window.innerWidth;
|
||||
ctx.canvas.height = window.innerHeight;
|
||||
let centerX = ctx.canvas.width / 2;
|
||||
let centerY = ctx.canvas.height / 2;
|
||||
|
||||
// ============================================
|
||||
// Initialize Core Systems
|
||||
// ============================================
|
||||
|
||||
let deg_per_sec = 10;
|
||||
let targetFps = 60;
|
||||
let frameDuration = 1000 / targetFps;
|
||||
// Create animation engine and connect it to the scene
|
||||
const engine = new AnimationEngine('myCanvas');
|
||||
engine.setScene(scene);
|
||||
|
||||
let rotation = 0; //was = j = angle
|
||||
let paused = false;
|
||||
let elapsedTime = 0;
|
||||
let lastTimestamp = 0;
|
||||
render_clear();
|
||||
// Initialize Scene UI
|
||||
const sceneUI = new SceneUI(scene, 'layers-container');
|
||||
|
||||
let drawObj = null;
|
||||
function createInstance(className, args) {
|
||||
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);
|
||||
// ============================================
|
||||
// Event Listeners
|
||||
// ============================================
|
||||
|
||||
let toolbarShowing = true;
|
||||
document.addEventListener("keydown", toggleSettings);
|
||||
|
||||
// Add resize event listener
|
||||
window.addEventListener('resize', function () {
|
||||
ctx.canvas.width = window.innerWidth;
|
||||
ctx.canvas.height = window.innerHeight;
|
||||
centerX = ctx.canvas.width / 2;
|
||||
centerY = ctx.canvas.height / 2;
|
||||
|
||||
});
|
||||
// ============================================
|
||||
// UI Control Functions
|
||||
// ============================================
|
||||
|
||||
function manualToggleSettings() {
|
||||
console.log("hi")
|
||||
toolbarShowing = !toolbarShowing;
|
||||
let tb = document.getElementById("toolbar");
|
||||
if (toolbarShowing) {
|
||||
tb.style.display = "flex";
|
||||
} else {
|
||||
tb.style.display = "none";
|
||||
}
|
||||
tb.style.display = toolbarShowing ? "flex" : "none";
|
||||
}
|
||||
|
||||
function toggleSettings(e) {
|
||||
@@ -130,43 +48,40 @@ function toggleSettings(e) {
|
||||
toolbarShowing = !toolbarShowing;
|
||||
}
|
||||
if (e.code === "Space") {
|
||||
paused = !paused;
|
||||
engine.togglePause();
|
||||
}
|
||||
|
||||
let tb = document.getElementById("toolbar");
|
||||
if (toolbarShowing) {
|
||||
tb.style.display = "flex";
|
||||
} else {
|
||||
tb.style.display = "none";
|
||||
}
|
||||
tb.style.display = toolbarShowing ? "flex" : "none";
|
||||
}
|
||||
|
||||
function TogglePause() {
|
||||
let pb = document.getElementById("pauseButton");
|
||||
paused = !paused;
|
||||
|
||||
if (paused) {
|
||||
pb.textContent = "Play";
|
||||
} else {
|
||||
pb.textContent = "Pause";
|
||||
}
|
||||
const paused = engine.togglePause();
|
||||
pb.textContent = paused ? "Play" : "Pause";
|
||||
}
|
||||
|
||||
function Reset() {
|
||||
rotation = 0; //was = j = angle
|
||||
currentFrame = 0;
|
||||
engine.reset();
|
||||
}
|
||||
|
||||
function ForwardFrame() {
|
||||
rotation += deg_per_sec / targetFps; // was = j = innerRotation, now = rotation
|
||||
currentFrame += 1; // was = i
|
||||
engine.stepForward();
|
||||
}
|
||||
|
||||
function BackwardFrame() {
|
||||
rotation -= deg_per_sec / targetFps; // was = j = innerRotation, now = rotation
|
||||
currentFrame -= 1; // was = i
|
||||
engine.stepBackward();
|
||||
}
|
||||
|
||||
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