Skip to content

Instantly share code, notes, and snippets.

@sufianrhazi
Created October 29, 2024 14:01
Show Gist options
  • Save sufianrhazi/385727c0300d177d8d411cd6bb18e50d to your computer and use it in GitHub Desktop.
Save sufianrhazi/385727c0300d177d8d411cd6bb18e50d to your computer and use it in GitHub Desktop.
import Gooey, { calc, field, mount, ref } from '@srhazi/gooey';
import type { Component, Field } from '@srhazi/gooey';
const NumericInput: Component<{
value: Field<number>;
id?: string;
type: string;
min: string;
max: string;
}> = ({ value, ...rest }) => (
<input
{...rest}
value={value}
on:input={(e, el) => value.set(el.valueAsNumber)}
/>
);
const Visualization: Component = (props, { onMount }) => {
const canvasRef = ref<HTMLCanvasElement>();
let ctx: CanvasRenderingContext2D | null = null;
// Input data, just X & Y coordinates
const x = field(40);
const y = field(30);
// Derived data, calculations that show angle & degrees
const angle = calc(() => {
const baseAngle = Math.atan(y.get() / x.get());
if (x.get() < 0) {
return baseAngle + Math.PI;
}
if (y.get() < 0) {
return baseAngle + 2 * Math.PI;
}
return baseAngle;
});
const degrees = calc(() => (angle.get() * 360) / (2 * Math.PI));
// A calculation which is re-evaluated when dependencies change, but doesn't return anything.
// This can be used to subscribe to multiple things at once.
const render = calc(() => {
update({
x: x.get(),
y: y.get(),
angle: angle.get(),
});
});
onMount(() => {
if (canvasRef.current) {
ctx = canvasRef.current.getContext('2d');
}
// By subscribing to our render calculation, it becomes "alive" and is recalculated when the x/y/angle changes
return render.subscribe(() => {});
});
const update = ({
x,
y,
angle,
}: {
x: number;
y: number;
angle: number;
}) => {
if (!ctx) {
return;
}
ctx.fillStyle = '#F4F2F4';
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fill();
ctx.save();
ctx.scale(ctx.canvas.width / 200, ctx.canvas.height / 200);
ctx.translate(100, 100);
// Draw outline for angle
ctx.beginPath();
ctx.save();
ctx.strokeStyle = '#C0C000';
ctx.setLineDash([2.5, 2.5]);
ctx.arc(0, 0, 20, 0, 2 * Math.PI, angle > 0);
ctx.stroke();
ctx.restore();
// Draw orange arc for angle
ctx.beginPath();
ctx.strokeStyle = '#C0C000';
ctx.fillStyle = '#808000';
ctx.arc(0, 0, 20, 0, -angle, angle > 0);
ctx.stroke();
ctx.lineTo(0, 0);
ctx.fill();
// Draw red line for X axis
ctx.beginPath();
ctx.strokeStyle = '#800000';
ctx.fillStyle = '#C00000';
ctx.moveTo(0, 0);
ctx.lineTo(0 + x, 0);
ctx.stroke();
// Draw green line for Y axis
ctx.beginPath();
ctx.strokeStyle = '#008000';
ctx.fillStyle = '#00C000';
ctx.moveTo(0 + x, 0);
ctx.lineTo(0 + x, 0 - y);
ctx.stroke();
// Draw blue line for hypotenuse
ctx.beginPath();
ctx.strokeStyle = '#0000C0';
ctx.fillStyle = '#000080';
ctx.moveTo(0, 0);
ctx.lineTo(0 + x, 0 - y);
ctx.stroke();
// Red X point
ctx.beginPath();
ctx.strokeStyle = '#800000';
ctx.fillStyle = '#C00000';
ctx.arc(0 + x, 0, 4, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
// Green Y point
ctx.beginPath();
ctx.strokeStyle = '#008000';
ctx.fillStyle = '#00C000';
ctx.arc(0 + x, 0 - y, 4, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
// Blue center point
ctx.beginPath();
ctx.strokeStyle = '#0000C0';
ctx.fillStyle = '#000080';
ctx.arc(0, 0, 4, 0, 2 * Math.PI);
ctx.stroke();
ctx.fill();
ctx.restore();
};
return (
<div>
<style>
{`
.triangle-canvas {
position: relative;
cursor: crosshair;
width: 400px;
height: auto;
}
.triangle-canvas-controls {
display: grid;
padding: 16px;
gap: 16px;
grid-template-columns: auto 1fr;
}
.triangle-angle-input {
text-align: right;
}
`}
</style>
<canvas
class="triangle-canvas"
ref={canvasRef}
width="800"
height="800"
on:mousemove={(e, el) => {
const box = el.getBoundingClientRect();
const mouseX =
2 * ((e.clientX - box.left) / box.width - 0.5);
const mouseY =
2 * ((e.clientY - box.top) / box.height - 0.5);
x.set(Math.round(mouseX * 1000) / 10);
y.set(Math.round(-mouseY * 1000) / 10);
}}
on:touchmove={(e: TouchEvent, el) => {
if (e.targetTouches[0]) {
e.preventDefault();
const box = el.getBoundingClientRect();
const touchX =
2 *
((e.targetTouches[0].clientX - box.left) /
box.width -
0.5);
const touchY =
2 *
((e.targetTouches[0].clientY - box.top) /
box.height -
0.5);
x.set(Math.round(touchX * 10000) / 10);
y.set(Math.round(-touchY * 1000) / 10);
}
}}
/>
<fieldset class="triangle-canvas-controls">
<legend>Controls</legend>
<label for="x-value">X Value</label>
<NumericInput
id="x-value"
type="number"
value={x}
min="-100"
max="100"
/>
<span />
<NumericInput type="range" value={x} min="-100" max="100" />
<label for="y-value">Y Value</label>
<NumericInput
id="y-value"
type="number"
value={y}
min="-100"
max="100"
/>
<span />
<NumericInput type="range" value={y} min="-100" max="100" />
<label for="angle">Angle</label>
<input
class="triangle-angle-input"
id="angle"
type="text"
value={calc(() => `${angle.get().toFixed(3)} radians`)}
readonly
/>
<label for="degrees">Degrees</label>
<input
class="triangle-angle-input"
id="degrees"
type="text"
value={calc(() => `${degrees.get().toFixed(3)} degrees`)}
readonly
/>
</fieldset>
</div>
);
};
const angleViz = document.getElementById('angleViz');
if (angleViz) {
mount(angleViz, <Visualization />);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment