Reworked event streaming
This commit is contained in:
+1
-2
@@ -3,5 +3,4 @@ import { ClientDrawService } from './services/draw/draw.client-service'
|
|||||||
|
|
||||||
const stateService = new ClientStateService()
|
const stateService = new ClientStateService()
|
||||||
const drawService = new ClientDrawService(stateService)
|
const drawService = new ClientDrawService(stateService)
|
||||||
drawService.draw()
|
stateService.connect(drawService)
|
||||||
stateService.connect()
|
|
||||||
@@ -5,6 +5,7 @@ export type CanvasState = {
|
|||||||
export type Stroke = {
|
export type Stroke = {
|
||||||
points: Point[],
|
points: Point[],
|
||||||
color: string,
|
color: string,
|
||||||
|
density: number,
|
||||||
width: number
|
width: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { Stroke } from "./canvas-state";
|
||||||
|
|
||||||
|
export type ListenCallbackParam = { strokeId: number, stroke: Stroke }
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Point } from "../../model/canvas-state";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a point on a Catmull-Rom spline segment at parameter t
|
||||||
|
*/
|
||||||
|
export function catmullRomPoint(
|
||||||
|
p0: Point,
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
p3: Point,
|
||||||
|
t: number
|
||||||
|
): Point {
|
||||||
|
const t2 = t * t;
|
||||||
|
const t3 = t2 * t;
|
||||||
|
|
||||||
|
const x =
|
||||||
|
0.5 *
|
||||||
|
(2 * p1.x +
|
||||||
|
(-p0.x + p2.x) * t +
|
||||||
|
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
||||||
|
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
|
||||||
|
|
||||||
|
const y =
|
||||||
|
0.5 *
|
||||||
|
(2 * p1.y +
|
||||||
|
(-p0.y + p2.y) * t +
|
||||||
|
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
||||||
|
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
|
||||||
|
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a list of raw input points into a smooth Catmull-Rom spline
|
||||||
|
*/
|
||||||
|
export function getCatmullRomPath(
|
||||||
|
points: Point[],
|
||||||
|
density: number,
|
||||||
|
): Point[] {
|
||||||
|
const segmentsPerInterval = density;
|
||||||
|
|
||||||
|
if (points.length === 0) return [];
|
||||||
|
if (points.length === 1) return [{ ...points[0] }];
|
||||||
|
|
||||||
|
const smoothed: Point[] = [];
|
||||||
|
const n = points.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < n - 1; i++) {
|
||||||
|
const p0 = points[Math.max(0, i - 1)];
|
||||||
|
const p1 = points[i];
|
||||||
|
const p2 = points[i + 1];
|
||||||
|
const p3 = points[Math.min(n - 1, i + 2)];
|
||||||
|
|
||||||
|
const step = 1 / segmentsPerInterval;
|
||||||
|
|
||||||
|
for (let s = 0; s <= segmentsPerInterval; s++) {
|
||||||
|
const t = s * step;
|
||||||
|
const pt = catmullRomPoint(p0, p1, p2, p3, t);
|
||||||
|
smoothed.push(pt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove near-duplicates at segment boundaries
|
||||||
|
const result = smoothed.filter((pt, idx, arr) => {
|
||||||
|
if (idx === 0) return true;
|
||||||
|
const prev = arr[idx - 1];
|
||||||
|
return Math.hypot(pt.x - prev.x, pt.y - prev.y) > 0.001;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBrushWidthAt(index: number, totalPoints: number, baseWidth: number = 12): number {
|
||||||
|
// Example: taper at start and end + speed-based variation
|
||||||
|
const t = index / (totalPoints - 1);
|
||||||
|
let width = baseWidth;
|
||||||
|
|
||||||
|
// Ease in / ease out
|
||||||
|
if (t < 0.1) width *= t * 10;
|
||||||
|
if (t > 0.9) width *= (1 - t) * 10;
|
||||||
|
|
||||||
|
return Math.max(1, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function drawVariableWidthSegment(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
p1: Point,
|
||||||
|
p2: Point,
|
||||||
|
width1: number,
|
||||||
|
width2: number
|
||||||
|
): void {
|
||||||
|
const dx = p2.x - p1.x;
|
||||||
|
const dy = p2.y - p1.y;
|
||||||
|
const len = Math.hypot(dx, dy); // More efficient than sqrt(dx*dx + dy*dy)
|
||||||
|
|
||||||
|
if (len < 0.001) return; // Points are too close
|
||||||
|
|
||||||
|
// Normalized perpendicular vector (rotated 90 degrees)
|
||||||
|
const nx = -dy / len;
|
||||||
|
const ny = dx / len;
|
||||||
|
|
||||||
|
const halfW1 = width1 / 2;
|
||||||
|
const halfW2 = width2 / 2;
|
||||||
|
|
||||||
|
// Four corners of the quadrilateral
|
||||||
|
const x1 = p1.x + nx * halfW1;
|
||||||
|
const y1 = p1.y + ny * halfW1;
|
||||||
|
const x2 = p1.x - nx * halfW1;
|
||||||
|
const y2 = p1.y - ny * halfW1;
|
||||||
|
const x3 = p2.x - nx * halfW2;
|
||||||
|
const y3 = p2.y - ny * halfW2;
|
||||||
|
const x4 = p2.x + nx * halfW2;
|
||||||
|
const y4 = p2.y + ny * halfW2;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x1, y1);
|
||||||
|
ctx.lineTo(x2, y2);
|
||||||
|
ctx.lineTo(x3, y3);
|
||||||
|
ctx.lineTo(x4, y4);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
import { Point, Stroke } from "../../model/canvas-state";
|
import { Point, Stroke } from "../../model/canvas-state";
|
||||||
import { ClientStateService } from "../state/state.client-service";
|
import { ClientStateService } from "../state/state.client-service";
|
||||||
|
import { getBrushWidthAt, drawVariableWidthSegment, getCatmullRomPath } from "./cat-mul-rom.util";
|
||||||
|
|
||||||
|
|
||||||
export class ClientDrawService {
|
export class ClientDrawService {
|
||||||
private readonly canvasContext: CanvasRenderingContext2D
|
private readonly cctx: CanvasRenderingContext2D
|
||||||
|
|
||||||
private currentColor
|
private currentColor
|
||||||
|
|
||||||
private currentWidth = 12;
|
private currentDensity
|
||||||
|
|
||||||
private currentStroke?: number
|
private currentWidth
|
||||||
|
|
||||||
|
private currentStrokeId?: number
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly stateService: ClientStateService,
|
private readonly stateService: ClientStateService,
|
||||||
private readonly canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement,
|
private readonly canvas: HTMLCanvasElement = document.getElementById("canvas") as HTMLCanvasElement,
|
||||||
private readonly colorPicker = document.getElementById('colorPicker')!,
|
private readonly colorPicker = document.getElementById('colorPicker')!,
|
||||||
|
|
||||||
|
private readonly densitySlider = document.getElementById('densitySlider')!,
|
||||||
|
private readonly densityValue = document.getElementById('densityValue')!,
|
||||||
|
|
||||||
private readonly widthSlider = document.getElementById('widthSlider')!,
|
private readonly widthSlider = document.getElementById('widthSlider')!,
|
||||||
private readonly widthValue = document.getElementById('widthValue')!,
|
private readonly widthValue = document.getElementById('widthValue')!,
|
||||||
|
|
||||||
private readonly menuButton = document.getElementById('menuButton')!,
|
private readonly menuButton = document.getElementById('menuButton')!,
|
||||||
private readonly drawer = document.getElementById('drawer')!,
|
private readonly drawer = document.getElementById('drawer')!,
|
||||||
private readonly closeButton = document.getElementById('closeButton')!,
|
private readonly closeButton = document.getElementById('closeButton')!,
|
||||||
@@ -25,33 +32,33 @@ export class ClientDrawService {
|
|||||||
this.resizeCanvas()
|
this.resizeCanvas()
|
||||||
this.setupCanvasEvents()
|
this.setupCanvasEvents()
|
||||||
window.addEventListener("resize", () => this.resizeCanvas());
|
window.addEventListener("resize", () => this.resizeCanvas());
|
||||||
this.canvasContext = canvas.getContext("2d")!
|
this.cctx = canvas.getContext("2d")!
|
||||||
this.currentColor = colorPicker?.getAttribute('value') ?? "#eaafff"
|
this.currentColor = colorPicker!.getAttribute('value')!
|
||||||
|
this.currentDensity = Number(densitySlider!.getAttribute('value'))!
|
||||||
|
this.currentWidth = Number(widthSlider!.getAttribute('value'))!
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
draw() {
|
||||||
|
|
||||||
|
console.log("drawing")
|
||||||
const strokes: Stroke[] = this.stateService.getStrokes()
|
const strokes: Stroke[] = this.stateService.getStrokes()
|
||||||
|
|
||||||
this.canvasContext.globalAlpha = 0.05;
|
this.cctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
this.canvasContext.fillStyle = "white";
|
|
||||||
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
|
||||||
this.canvasContext.globalAlpha = 1;
|
|
||||||
|
|
||||||
|
this.cctx.globalAlpha = 1;
|
||||||
strokes.forEach(stroke => {
|
strokes.forEach(stroke => {
|
||||||
this.canvasContext.fillStyle = stroke.color;
|
this.cctx.fillStyle = stroke.color;
|
||||||
this.drawCurve(stroke.points, stroke.width)
|
this.drawCurve(stroke.points, stroke.density, stroke.width)
|
||||||
})
|
})
|
||||||
|
|
||||||
requestAnimationFrame(() => this.draw())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private addPointToStroke = (e: any) => {
|
private addPointToStroke = (e: any) => {
|
||||||
if (this.currentStroke === undefined) {
|
if (this.currentStrokeId === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
this.stateService.addPoint(this.currentStroke, {
|
this.stateService.addPoint(this.currentStrokeId, {
|
||||||
x: e.clientX - rect.left,
|
x: e.clientX - rect.left,
|
||||||
y: e.clientY - rect.top,
|
y: e.clientY - rect.top,
|
||||||
})
|
})
|
||||||
@@ -59,10 +66,10 @@ export class ClientDrawService {
|
|||||||
|
|
||||||
private beginStroke = (e: any) => {
|
private beginStroke = (e: any) => {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
|
||||||
this.stateService.beginStroke({
|
this.stateService.beginStroke({
|
||||||
color: this.currentColor,
|
color: this.currentColor,
|
||||||
width: this.currentWidth,
|
width: this.currentWidth,
|
||||||
|
density: this.currentDensity,
|
||||||
points: [{
|
points: [{
|
||||||
x: e.clientX - rect.left,
|
x: e.clientX - rect.left,
|
||||||
y: e.clientY - rect.top
|
y: e.clientY - rect.top
|
||||||
@@ -70,7 +77,7 @@ export class ClientDrawService {
|
|||||||
,
|
,
|
||||||
}).then((id: number) => {
|
}).then((id: number) => {
|
||||||
console.log("begin stroke", id)
|
console.log("begin stroke", id)
|
||||||
this.currentStroke = id;
|
this.currentStrokeId = id;
|
||||||
this.canvas.addEventListener("mousemove", this.addPointToStroke)
|
this.canvas.addEventListener("mousemove", this.addPointToStroke)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -99,6 +106,12 @@ export class ClientDrawService {
|
|||||||
this.currentWidth = Number(target.value)
|
this.currentWidth = Number(target.value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.densitySlider?.addEventListener('input', (e) => {
|
||||||
|
const target = e.target as HTMLTextAreaElement
|
||||||
|
this.densityValue!.innerHTML = target.value
|
||||||
|
this.currentDensity = Number(target.value)
|
||||||
|
});
|
||||||
|
|
||||||
this.menuButton.addEventListener('click', () => {
|
this.menuButton.addEventListener('click', () => {
|
||||||
this.drawer.classList.toggle('open');
|
this.drawer.classList.toggle('open');
|
||||||
});
|
});
|
||||||
@@ -125,138 +138,18 @@ export class ClientDrawService {
|
|||||||
this.canvas.height = size;
|
this.canvas.height = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawCurve(points: Point[], density: number) {
|
private drawCurve(points: Point[], density: number, width: number) {
|
||||||
if (points.length < 2) return;
|
if (points.length < 2) return;
|
||||||
|
|
||||||
|
points = getCatmullRomPath(points, density)
|
||||||
|
|
||||||
for (let i = 0; i < points.length - 1; i++) {
|
for (let i = 0; i < points.length - 1; i++) {
|
||||||
const width1 = getBrushWidthAt(i, points.length);
|
const width1 = getBrushWidthAt(i, points.length, width);
|
||||||
const width2 = getBrushWidthAt(i + 1, points.length);
|
const width2 = getBrushWidthAt(i + 1, points.length, width);
|
||||||
const p1 = points[i];
|
const p1 = points[i];
|
||||||
const p2 = points[i + 1];
|
const p2 = points[i + 1];
|
||||||
drawVariableWidthSegment(this.canvasContext, p1, p2, width1, width2);
|
drawVariableWidthSegment(this.cctx, p1, p2, width1, width2);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a point on a Catmull-Rom spline segment at parameter t
|
|
||||||
*/
|
|
||||||
export function catmullRomPoint(
|
|
||||||
p0: Point,
|
|
||||||
p1: Point,
|
|
||||||
p2: Point,
|
|
||||||
p3: Point,
|
|
||||||
t: number
|
|
||||||
): Point {
|
|
||||||
const t2 = t * t;
|
|
||||||
const t3 = t2 * t;
|
|
||||||
|
|
||||||
const x =
|
|
||||||
0.5 *
|
|
||||||
(2 * p1.x +
|
|
||||||
(-p0.x + p2.x) * t +
|
|
||||||
(2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
|
|
||||||
(-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3);
|
|
||||||
|
|
||||||
const y =
|
|
||||||
0.5 *
|
|
||||||
(2 * p1.y +
|
|
||||||
(-p0.y + p2.y) * t +
|
|
||||||
(2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
|
|
||||||
(-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3);
|
|
||||||
|
|
||||||
return { x, y };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a list of raw input points into a smooth Catmull-Rom spline
|
|
||||||
*/
|
|
||||||
export function getCatmullRomPath(
|
|
||||||
points: Point[],
|
|
||||||
density: number,
|
|
||||||
): Point[] {
|
|
||||||
const segmentsPerInterval = density;
|
|
||||||
|
|
||||||
if (points.length === 0) return [];
|
|
||||||
if (points.length === 1) return [{ ...points[0] }];
|
|
||||||
|
|
||||||
const smoothed: Point[] = [];
|
|
||||||
const n = points.length;
|
|
||||||
|
|
||||||
for (let i = 0; i < n - 1; i++) {
|
|
||||||
const p0 = points[Math.max(0, i - 1)];
|
|
||||||
const p1 = points[i];
|
|
||||||
const p2 = points[i + 1];
|
|
||||||
const p3 = points[Math.min(n - 1, i + 2)];
|
|
||||||
|
|
||||||
const step = 1 / segmentsPerInterval;
|
|
||||||
|
|
||||||
for (let s = 0; s <= segmentsPerInterval; s++) {
|
|
||||||
const t = s * step;
|
|
||||||
const pt = catmullRomPoint(p0, p1, p2, p3, t);
|
|
||||||
smoothed.push(pt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove near-duplicates at segment boundaries
|
|
||||||
const result = smoothed.filter((pt, idx, arr) => {
|
|
||||||
if (idx === 0) return true;
|
|
||||||
const prev = arr[idx - 1];
|
|
||||||
return Math.hypot(pt.x - prev.x, pt.y - prev.y) > 0.001;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBrushWidthAt(index: number, totalPoints: number, baseWidth: number = 12): number {
|
|
||||||
// Example: taper at start and end + speed-based variation
|
|
||||||
const t = index / (totalPoints - 1);
|
|
||||||
let width = baseWidth;
|
|
||||||
|
|
||||||
// Ease in / ease out
|
|
||||||
if (t < 0.1) width *= t * 10;
|
|
||||||
if (t > 0.9) width *= (1 - t) * 10;
|
|
||||||
|
|
||||||
return Math.max(1, width);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function drawVariableWidthSegment(
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
p1: Point,
|
|
||||||
p2: Point,
|
|
||||||
width1: number,
|
|
||||||
width2: number
|
|
||||||
): void {
|
|
||||||
const dx = p2.x - p1.x;
|
|
||||||
const dy = p2.y - p1.y;
|
|
||||||
const len = Math.hypot(dx, dy); // More efficient than sqrt(dx*dx + dy*dy)
|
|
||||||
|
|
||||||
if (len < 0.001) return; // Points are too close
|
|
||||||
|
|
||||||
// Normalized perpendicular vector (rotated 90 degrees)
|
|
||||||
const nx = -dy / len;
|
|
||||||
const ny = dx / len;
|
|
||||||
|
|
||||||
const halfW1 = width1 / 2;
|
|
||||||
const halfW2 = width2 / 2;
|
|
||||||
|
|
||||||
// Four corners of the quadrilateral
|
|
||||||
const x1 = p1.x + nx * halfW1;
|
|
||||||
const y1 = p1.y + ny * halfW1;
|
|
||||||
const x2 = p1.x - nx * halfW1;
|
|
||||||
const y2 = p1.y - ny * halfW1;
|
|
||||||
const x3 = p2.x - nx * halfW2;
|
|
||||||
const y3 = p2.y - ny * halfW2;
|
|
||||||
const x4 = p2.x + nx * halfW2;
|
|
||||||
const y4 = p2.y + ny * halfW2;
|
|
||||||
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(x1, y1);
|
|
||||||
ctx.lineTo(x2, y2);
|
|
||||||
ctx.lineTo(x3, y3);
|
|
||||||
ctx.lineTo(x4, y4);
|
|
||||||
ctx.closePath();
|
|
||||||
|
|
||||||
ctx.fill();
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,45 @@
|
|||||||
import { RPCSocket } from "../../../../node_modules/rpclibrary/js/Index";
|
import { RPCSocket } from "../../../../node_modules/rpclibrary/js/Index";
|
||||||
import { CanvasState, Point, Stroke } from "../../model/canvas-state";
|
import { CanvasState, Point, Stroke } from "../../model/canvas-state";
|
||||||
|
import { ListenCallbackParam } from "../../model/rpc-callbacks";
|
||||||
|
import { ClientDrawService } from "../draw/draw.client-service";
|
||||||
|
|
||||||
|
|
||||||
export class ClientStateService {
|
export class ClientStateService {
|
||||||
|
|
||||||
private remoteService: any
|
private remoteService: any
|
||||||
|
|
||||||
private canvasState: CanvasState = { strokes: [] }
|
private canvasState: CanvasState = { strokes: [] }
|
||||||
|
|
||||||
getStrokes() {
|
async connect(drawService: ClientDrawService) {
|
||||||
return this.canvasState?.strokes ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect() {
|
|
||||||
const sock = await new RPCSocket(8080, 'localhost').connect();
|
const sock = await new RPCSocket(8080, 'localhost').connect();
|
||||||
this.remoteService = sock['StateService']
|
this.remoteService = sock['StateService']
|
||||||
this.canvasState = await this.remoteService.listen((state: CanvasState) => {
|
this.canvasState = await this.getState()
|
||||||
this.canvasState = state
|
await this.remoteService.listen((listenDto: ListenCallbackParam) => {
|
||||||
|
if(!this.canvasState.strokes[listenDto.strokeId]){
|
||||||
|
this.canvasState.strokes[listenDto.strokeId] = listenDto.stroke
|
||||||
|
}else{
|
||||||
|
this.canvasState.strokes[listenDto.strokeId].points = [...this.canvasState.strokes[listenDto.strokeId].points, ...listenDto.stroke.points]
|
||||||
|
}
|
||||||
|
drawService.draw()
|
||||||
})
|
})
|
||||||
|
drawService.draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getState = async () => {
|
||||||
|
return await this.remoteService.getState()
|
||||||
|
}
|
||||||
|
|
||||||
beginStroke = async (stroke: Stroke) => {
|
beginStroke = async (stroke: Stroke) => {
|
||||||
return await this.remoteService.beginStroke(stroke)
|
return await this.remoteService.beginStroke(stroke)
|
||||||
}
|
}
|
||||||
|
|
||||||
addPoint = async (strokeId: number, point: Point) =>{
|
addPoint = async (strokeId: number, point: Point) => {
|
||||||
return this.remoteService.addPoint(strokeId, point)
|
return this.remoteService.addPoint(strokeId, point)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStrokes() {
|
||||||
|
return this.canvasState?.strokes ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
+18
-7
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
@@ -55,7 +56,8 @@
|
|||||||
.drawer {
|
.drawer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: -500px; /* Fully hidden when closed */
|
right: -500px;
|
||||||
|
/* Fully hidden when closed */
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background: white;
|
background: white;
|
||||||
@@ -137,10 +139,11 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- Canvas Area -->
|
<!-- Canvas Area -->
|
||||||
<div class="canvas-container">
|
<div class="canvas-container">
|
||||||
<canvas id="canvas" width="1200" height="800"></canvas>
|
<canvas id="canvas" width="450" height="450"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Burger Menu Button -->
|
<!-- Burger Menu Button -->
|
||||||
@@ -156,17 +159,25 @@
|
|||||||
<!-- Color Picker -->
|
<!-- Color Picker -->
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label for="colorPicker">Color</label>
|
<label for="colorPicker">Color</label>
|
||||||
<input type="color" id="colorPicker" value="#000000">
|
<input type="color" id="colorPicker" value="#eaafff">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Brush Width -->
|
<!-- density -->
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label for="widthSlider">Brush Size</label>
|
<label for="densitySlider">Smoothness</label>
|
||||||
<input type="range" id="widthSlider" min="1" max="100" value="12">
|
<input type="range" id="densitySlider" min="1" max="50" value="35">
|
||||||
<span id="widthValue" class="value">12 px</span>
|
<span id="densityValue" class="value">35</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- width -->
|
||||||
|
<div class="control">
|
||||||
|
<label for="widthSlider">Width</label>
|
||||||
|
<input type="range" id="widthSlider" min="1" max="50" value="35">
|
||||||
|
<span id="widthValue" class="value">35</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="main.js"></script>
|
<script type="module" src="main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Singleton, Initializable } from "depents";
|
import { Singleton, Initializable } from "depents";
|
||||||
import { RPCExporter } from "rpclibrary";
|
import { RPCExporter } from "rpclibrary";
|
||||||
import { CanvasState, Point, Stroke } from "../../../client/model/canvas-state";
|
import { CanvasState, Point, Stroke } from "../../../client/model/canvas-state";
|
||||||
|
import { ListenCallbackParam } from "../../../client/model/rpc-callbacks";
|
||||||
|
|
||||||
@Singleton()
|
@Singleton()
|
||||||
export class StateService implements Initializable, RPCExporter {
|
export class StateService implements Initializable, RPCExporter {
|
||||||
name = 'StateService' as const
|
name = 'StateService' as const
|
||||||
|
|
||||||
private canvasState: CanvasState = { strokes: [] }
|
private canvasState: CanvasState = { strokes: [] }
|
||||||
private clients: Array<(state: any) => void> = []
|
private clients: Array<(param: ListenCallbackParam) => void> = []
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
this.canvasState = { strokes: [] }
|
this.canvasState = { strokes: [] }
|
||||||
@@ -17,32 +18,34 @@ export class StateService implements Initializable, RPCExporter {
|
|||||||
beginStroke = async (stroke: Stroke) => {
|
beginStroke = async (stroke: Stroke) => {
|
||||||
const strokeId = this.canvasState.strokes.length
|
const strokeId = this.canvasState.strokes.length
|
||||||
this.canvasState.strokes.push(stroke)
|
this.canvasState.strokes.push(stroke)
|
||||||
console.log("beginStroke", strokeId, stroke)
|
this.updateclients(strokeId, stroke)
|
||||||
this.updateclients()
|
|
||||||
return strokeId
|
return strokeId
|
||||||
}
|
}
|
||||||
|
|
||||||
addPoint = async (strokeId: number, point: Point) => {
|
addPoint = async (strokeId: number, point: Point) => {
|
||||||
this.canvasState.strokes.length
|
|
||||||
this.canvasState.strokes[strokeId].points.push(point)
|
this.canvasState.strokes[strokeId].points.push(point)
|
||||||
this.updateclients()
|
this.updateclients(strokeId, { ...this.canvasState.strokes[strokeId], points: [point] })
|
||||||
}
|
}
|
||||||
|
|
||||||
listen = async (callback: (state: any) => Promise<void>) => {
|
listen = async (callback: (state: any) => Promise<void>) => {
|
||||||
await callback(this.canvasState)
|
|
||||||
this.clients = [...this.clients, callback]
|
this.clients = [...this.clients, callback]
|
||||||
return this.canvasState
|
return this.canvasState
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateclients = () => {
|
getState = async (): Promise<CanvasState> => {
|
||||||
|
return this.canvasState
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateclients = (strokeId: number, stroke: Stroke) => {
|
||||||
this.clients.forEach((client) => {
|
this.clients.forEach((client) => {
|
||||||
client(this.canvasState)
|
client({ strokeId, stroke })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
RPCs = [
|
RPCs = [
|
||||||
this.beginStroke,
|
this.beginStroke,
|
||||||
this.addPoint,
|
this.addPoint,
|
||||||
|
this.getState,
|
||||||
{
|
{
|
||||||
name: 'listen' as const,
|
name: 'listen' as const,
|
||||||
hook: (cb: any) => { this.listen(cb) }
|
hook: (cb: any) => { this.listen(cb) }
|
||||||
|
|||||||
Reference in New Issue
Block a user