Files
konva/test/sandbox.html
2025-08-23 03:18:14 -05:00

754 lines
24 KiB
HTML

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>KonvaJS Sandbox</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=1.0, minimum-scale=1.0, maximum-scale=1.0"
/>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #f0f0f0;
}
.main-container {
max-width: 1400px;
margin: 0 auto;
}
.section {
background: white;
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.demo-container {
display: flex;
gap: 20px;
min-height: 400px;
}
.demo-section {
flex: 1;
position: relative;
border-radius: 8px;
overflow: hidden;
}
.default-filters {
border-top: 3px solid #e74c3c;
}
.custom-filters {
border-top: 3px solid #27ae60;
}
.controls {
background: rgba(255, 255, 255, 0.95);
top: 10px;
left: 10px;
padding: 15px;
border-radius: 8px;
border: 2px solid #ddd;
min-width: 250px;
max-height: 350px;
overflow-y: auto;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.control-group {
margin-bottom: 12px;
}
.control-group:last-child {
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
font-size: 12px;
}
input[type='range'] {
width: 100%;
margin-bottom: 3px;
}
.value-display {
color: #666;
font-size: 12px;
}
.filter-buttons {
display: flex;
gap: 8px;
margin-bottom: 15px;
}
button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
font-size: 11px;
transition: all 0.3s ease;
}
button.active {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.emboss-btn {
background: #3498db;
color: white;
}
.emboss-btn.active {
background: #2980b9;
}
.solarize-btn {
background: #f39c12;
color: white;
}
.solarize-btn.active {
background: #e67e22;
}
select {
width: 100%;
padding: 3px;
border-radius: 3px;
border: 1px solid #ddd;
font-size: 11px;
}
input[type='checkbox'] {
margin-right: 5px;
}
</style>
<!-- <script src="https://cdn.rawgit.com/hammerjs/touchemulator/master/touch-emulator.js"></script> -->
<!-- <script src="https://unpkg.com/gifler@0.1.0/gifler.min.js"></script> -->
<script>
// TouchEmulator();
</script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.7/hammer.js"></script> -->
<!-- <script src="https://cdn.rawgit.com/hammerjs/touchemulator/master/touch-emulator.js"></script> -->
<!-- <script src="./hammer.js"></script> -->
<!-- <script src="https://unpkg.com/fabric@5.2.1/dist/fabric.js"></script> -->
</head>
<body>
<div class="main-container">
<h1 style="text-align: center; margin-bottom: 30px; color: #333">
Filter Comparison: Default vs Custom
</h1>
<div class="section">
<div class="demo-container">
<!-- Default Filters -->
<div class="demo-section default-filters">
<div
id="default-container"
style="width: 100%; height: 400px"
></div>
<div class="controls">
<h3 style="margin: 0 0 10px 0; color: #e74c3c; font-size: 14px">
Default Konva Filters
</h3>
<div class="filter-buttons">
<button id="emboss-default" class="emboss-btn active">
Emboss
</button>
<button id="solarize-default" class="solarize-btn">
Solarize
</button>
</div>
<!-- Default Emboss Controls -->
<div id="emboss-controls-default">
<div class="control-group">
<label for="emboss-strength-default"
>Strength:
<span
id="emboss-strength-value-default"
class="value-display"
>0.5</span
></label
>
<input
type="range"
id="emboss-strength-default"
min="0"
max="1"
value="0.5"
step="0.1"
/>
</div>
<div class="control-group">
<label for="emboss-white-default"
>White Level:
<span id="emboss-white-value-default" class="value-display"
>0.5</span
></label
>
<input
type="range"
id="emboss-white-default"
min="0"
max="1"
value="0.5"
step="0.1"
/>
</div>
<div class="control-group">
<label for="emboss-direction-default">Direction:</label>
<select id="emboss-direction-default">
<option value="top-left" selected>Top Left</option>
<option value="top">Top</option>
<option value="top-right">Top Right</option>
<option value="right">Right</option>
<option value="bottom-right">Bottom Right</option>
<option value="bottom">Bottom</option>
<option value="bottom-left">Bottom Left</option>
<option value="left">Left</option>
</select>
</div>
<div class="control-group">
<label
><input type="checkbox" id="emboss-blend-default" /> Blend
Mode</label
>
</div>
</div>
<!-- Default Solarize Controls -->
<div id="solarize-controls-default" style="display: none">
<div class="control-group">
<label for="solarize-threshold-default"
>Threshold:
<span
id="solarize-threshold-value-default"
class="value-display"
>127</span
></label
>
<input
type="range"
id="solarize-threshold-default"
min="0"
max="255"
value="127"
step="1"
/>
</div>
</div>
</div>
</div>
<!-- Custom Filters -->
<div class="demo-section custom-filters">
<div id="custom-container" style="width: 100%; height: 400px"></div>
<div class="controls">
<h3 style="margin: 0 0 10px 0; color: #27ae60; font-size: 14px">
Custom Implementations
</h3>
<div class="filter-buttons">
<button id="emboss-custom" class="emboss-btn active">
Emboss
</button>
<button id="solarize-custom" class="solarize-btn">
Solarize
</button>
</div>
<!-- Custom Emboss Controls -->
<div id="emboss-controls-custom">
<div class="control-group">
<label for="emboss-strength-custom"
>Strength:
<span
id="emboss-strength-value-custom"
class="value-display"
>1</span
></label
>
<input
type="range"
id="emboss-strength-custom"
min="0"
max="3"
value="1"
step="0.1"
/>
</div>
</div>
<!-- Custom Solarize Controls -->
<div id="solarize-controls-custom" style="display: none">
<div class="control-group">
<label for="solarize-threshold-custom"
>Threshold:
<span
id="solarize-threshold-value-custom"
class="value-display"
>128</span
></label
>
<input
type="range"
id="solarize-threshold-custom"
min="0"
max="255"
value="128"
step="1"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module">
import Konva from '../src/index.ts';
// ===============================
// Custom filter implementations
// ===============================
function solarizeImageData(imageData, threshold = 128) {
const d = imageData.data;
for (let i = 0; i < d.length; i += 4) {
const r = d[i],
g = d[i + 1],
b = d[i + 2];
// sRGB luma
const L = 0.2126 * r + 0.7152 * g + 0.0722 * b;
if (L >= threshold) {
d[i] = 255 - r;
d[i + 1] = 255 - g;
d[i + 2] = 255 - b;
}
}
return imageData;
}
function embossImageData(imageData) {
const data = imageData.data;
const w = imageData.width;
const h = imageData.height;
debugger;
// Inputs from Konva node
const strength01 = Math.min(
1,
Math.max(0, this.embossStrength?.() ?? 0.5)
); // [0..1]
const whiteLevel01 = Math.min(
1,
Math.max(0, this.embossWhiteLevel?.() ?? 0.5)
); // [0..1]
// Convert string direction to degrees
const directionMap = {
'top-left': 315,
top: 270,
'top-right': 225,
right: 180,
'bottom-right': 135,
bottom: 90,
'bottom-left': 45,
left: 0,
};
const directionDeg =
directionMap[this.embossDirection?.() ?? 'top-left'] ?? 315; // degrees
const blend = !!(this.embossBlend?.() ?? false);
// Internal mapping:
// - Pixastic "strength" was 0..10; we honor your 0..1 API and scale accordingly.
// - Sobel directional response is roughly in [-1020..1020] for 8-bit luminance; scale to ~±128.
const strength = strength01 * 10;
const bias = whiteLevel01 * 255;
const dirRad = (directionDeg * Math.PI) / 180;
const cx = Math.cos(dirRad);
const cy = Math.sin(dirRad);
const SCALE = (128 / 1020) * strength; // ≈0.1255 * strength
// Precompute luminance (Rec.709)
const src = new Uint8ClampedArray(data); // snapshot
const lum = new Float32Array(w * h);
for (let p = 0, i = 0; i < data.length; i += 4, p++) {
lum[p] = 0.2126 * src[i] + 0.7152 * src[i + 1] + 0.0722 * src[i + 2];
}
// Sobel kernels (flattened)
const Gx = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const Gy = [-1, -2, -1, 0, 0, 0, 1, 2, 1];
// neighbor offsets around center pixel in lum space
const OFF = [-w - 1, -w, -w + 1, -1, 0, 1, w - 1, w, w + 1];
// Helpers
const clamp8 = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);
// Process: leave a 1px border unchanged (faster/cleaner)
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
const p = y * w + x;
// Directional derivative = (cosθ * Gx + sinθ * Gy) • neighborhood(lum)
let sx = 0,
sy = 0;
// unroll loop for speed
sx += lum[p + OFF[0]] * Gx[0];
sy += lum[p + OFF[0]] * Gy[0];
sx += lum[p + OFF[1]] * Gx[1];
sy += lum[p + OFF[1]] * Gy[1];
sx += lum[p + OFF[2]] * Gx[2];
sy += lum[p + OFF[2]] * Gy[2];
sx += lum[p + OFF[3]] * Gx[3];
sy += lum[p + OFF[3]] * Gy[3];
// center has 0 weights in both Sobel masks; can skip if desired
sx += lum[p + OFF[5]] * Gx[5];
sy += lum[p + OFF[5]] * Gy[5];
sx += lum[p + OFF[6]] * Gx[6];
sy += lum[p + OFF[6]] * Gy[6];
sx += lum[p + OFF[7]] * Gx[7];
sy += lum[p + OFF[7]] * Gy[7];
sx += lum[p + OFF[8]] * Gx[8];
sy += lum[p + OFF[8]] * Gy[8];
const r = cx * sx + cy * sy; // directional response
const outGray = clamp8(bias + r * SCALE); // biased, scaled, clamped
const o = p * 4;
if (blend) {
// Add the emboss "relief" around chosen bias to original RGB
const delta = outGray - bias; // symmetric around whiteLevel
data[o] = clamp8(src[o] + delta);
data[o + 1] = clamp8(src[o + 1] + delta);
data[o + 2] = clamp8(src[o + 2] + delta);
data[o + 3] = src[o + 3];
} else {
// Grayscale embossed output
data[o] = data[o + 1] = data[o + 2] = outGray;
data[o + 3] = src[o + 3];
}
}
}
// Copy border (untouched) to keep edges clean
// top & bottom rows
for (let x = 0; x < w; x++) {
let oTop = x * 4,
oBot = ((h - 1) * w + x) * 4;
data[oTop] = src[oTop];
data[oTop + 1] = src[oTop + 1];
data[oTop + 2] = src[oTop + 2];
data[oTop + 3] = src[oTop + 3];
data[oBot] = src[oBot];
data[oBot + 1] = src[oBot + 1];
data[oBot + 2] = src[oBot + 2];
data[oBot + 3] = src[oBot + 3];
}
// left & right columns
for (let y = 1; y < h - 1; y++) {
let oL = y * w * 4,
oR = (y * w + (w - 1)) * 4;
data[oL] = src[oL];
data[oL + 1] = src[oL + 1];
data[oL + 2] = src[oL + 2];
data[oL + 3] = src[oL + 3];
data[oR] = src[oR];
data[oR + 1] = src[oR + 1];
data[oR + 2] = src[oR + 2];
data[oR + 3] = src[oR + 3];
}
return imageData;
}
// Add custom properties
// Konva.Factory.addGetterSetter(Konva.Node, 'solarizeThreshold', 128);
// Konva.Factory.addGetterSetter(Konva.Node, 'embossStrengthCustom', 1);
const containerWidth = 400;
const containerHeight = 400;
// ===============================
// Default Filters Stage
// ===============================
const defaultStage = new Konva.Stage({
container: 'default-container',
width: containerWidth,
height: containerHeight,
});
const defaultLayer = new Konva.Layer();
defaultStage.add(defaultLayer);
// ===============================
// Custom Filters Stage
// ===============================
const customStage = new Konva.Stage({
container: 'custom-container',
width: containerWidth,
height: containerHeight,
});
const customLayer = new Konva.Layer();
customStage.add(customLayer);
// Load image and create both versions
const imageObj = new Image();
imageObj.onload = function () {
// Default filters image
const defaultImage = new Konva.Image({
image: imageObj,
x: 50,
y: 50,
width: 300,
height: 225,
draggable: true,
});
defaultImage.cache({ pixelRatio: 2 });
defaultImage.filters([Konva.Filters.Emboss]);
defaultLayer.add(defaultImage);
defaultLayer.draw();
// Custom filters image
const customImage = new Konva.Image({
image: imageObj,
x: 50,
y: 50,
width: 300,
height: 225,
draggable: true,
});
customImage.cache({ pixelRatio: 2 });
customImage.embossStrength(
parseFloat(document.getElementById('emboss-strength-default').value)
);
customImage.embossWhiteLevel(
parseFloat(document.getElementById('emboss-white-default').value)
);
customImage.embossDirection(
document.getElementById('emboss-direction-default').value
);
customImage.embossBlend(
document.getElementById('emboss-blend-default').checked
);
customImage.filters([embossImageData]);
customLayer.add(customImage);
customLayer.draw();
// ===============================
// Control Logic
// ===============================
let currentDefaultFilter = 'emboss';
let currentCustomFilter = 'emboss';
function updateDefaultFilter() {
if (currentDefaultFilter === 'emboss') {
defaultImage.filters([Konva.Filters.Emboss]);
defaultImage.embossStrength(
parseFloat(
document.getElementById('emboss-strength-default').value
)
);
defaultImage.embossWhiteLevel(
parseFloat(document.getElementById('emboss-white-default').value)
);
defaultImage.embossDirection(
document.getElementById('emboss-direction-default').value
);
defaultImage.embossBlend(
document.getElementById('emboss-blend-default').checked
);
} else {
// Use custom solarize function for default filter to add threshold control
defaultImage.filters([Konva.Filters.Solarize]);
}
defaultLayer.batchDraw();
updateCustomFilter();
}
function updateCustomFilter() {
console.log(currentCustomFilter);
if (currentCustomFilter === 'emboss') {
customImage.embossStrength(
parseFloat(
document.getElementById('emboss-strength-default').value
)
);
customImage.embossWhiteLevel(
parseFloat(document.getElementById('emboss-white-default').value)
);
customImage.embossDirection(
document.getElementById('emboss-direction-default').value
);
customImage.embossBlend(
document.getElementById('emboss-blend-default').checked
);
customImage.filters([embossImageData]);
} else {
customImage.filters([
(imageData) =>
solarizeImageData(
imageData,
parseFloat(
document.getElementById('solarize-threshold-custom').value
)
),
]);
}
customLayer.batchDraw();
}
// Default filter button controls
document
.getElementById('emboss-default')
.addEventListener('click', () => {
currentDefaultFilter = 'emboss';
document.getElementById('emboss-default').classList.add('active');
document
.getElementById('solarize-default')
.classList.remove('active');
document.getElementById('emboss-controls-default').style.display =
'block';
document.getElementById('solarize-controls-default').style.display =
'none';
updateDefaultFilter();
updateCustomFilter();
});
document
.getElementById('solarize-default')
.addEventListener('click', () => {
currentDefaultFilter = 'solarize';
document.getElementById('solarize-default').classList.add('active');
document
.getElementById('emboss-default')
.classList.remove('active');
document.getElementById('solarize-controls-default').style.display =
'block';
document.getElementById('emboss-controls-default').style.display =
'none';
updateDefaultFilter();
updateCustomFilter();
});
// Custom filter button controls
document
.getElementById('emboss-custom')
.addEventListener('click', () => {
currentCustomFilter = 'emboss';
document.getElementById('emboss-custom').classList.add('active');
document
.getElementById('solarize-custom')
.classList.remove('active');
document.getElementById('emboss-controls-custom').style.display =
'block';
document.getElementById('solarize-controls-custom').style.display =
'none';
updateCustomFilter();
});
document
.getElementById('solarize-custom')
.addEventListener('click', () => {
currentCustomFilter = 'solarize';
document.getElementById('solarize-custom').classList.add('active');
document.getElementById('emboss-custom').classList.remove('active');
document.getElementById('solarize-controls-custom').style.display =
'block';
document.getElementById('emboss-controls-custom').style.display =
'none';
updateCustomFilter();
});
// Default emboss controls
document
.getElementById('emboss-strength-default')
.addEventListener('input', (e) => {
document.getElementById(
'emboss-strength-value-default'
).textContent = e.target.value;
updateDefaultFilter();
updateCustomFilter;
});
document
.getElementById('emboss-white-default')
.addEventListener('input', (e) => {
document.getElementById('emboss-white-value-default').textContent =
e.target.value;
updateDefaultFilter();
updateCustomFilter;
});
document
.getElementById('emboss-direction-default')
.addEventListener('change', () => {
updateDefaultFilter();
updateCustomFilter();
});
document
.getElementById('emboss-blend-default')
.addEventListener('change', () => {
updateDefaultFilter();
updateCustomFilter();
});
// Default solarize controls
document
.getElementById('solarize-threshold-default')
.addEventListener('input', (e) => {
document.getElementById(
'solarize-threshold-value-default'
).textContent = e.target.value;
updateDefaultFilter();
updateCustomFilter();
});
// Custom filter controls
document
.getElementById('emboss-strength-custom')
.addEventListener('input', (e) => {
document.getElementById(
'emboss-strength-value-custom'
).textContent = e.target.value;
updateCustomFilter();
});
document
.getElementById('solarize-threshold-custom')
.addEventListener('input', (e) => {
document.getElementById(
'solarize-threshold-value-custom'
).textContent = e.target.value;
updateCustomFilter();
});
};
imageObj.crossOrigin = 'anonymous';
imageObj.src = 'https://konvajs.org/assets/darth-vader.jpg';
</script>
</body>
</html>