mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
remove all unused files
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
{"cmt_scan":[{"also_scan_build_root":true,"build_root":"lib/bs","scan_dirs":["src/modules/client/hooks/rescript","src/modules/client/hooks/rescript/canvas"]}],"dirs":["src/modules/client/hooks/rescript","src/modules/client/hooks/rescript/canvas"],"generated":[],"pkgs":[["@rescript/react","/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+react@0.15.0_@rescript+runtime@12.3.0_react-dom@19.2.5_react@19.2.5__react@19.2.5/node_modules/@rescript/react"]],"version":2}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "12.3.0",
|
|
||||||
"bsc_path": "/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+linux-x64@12.3.0/node_modules/@rescript/linux-x64/bin/bsc.exe",
|
|
||||||
"bsc_hash": "75e3a59c95cc953608fd3dd6ea6ed68141bba4414a20a52f114261b8a48385fa",
|
|
||||||
"rescript_config_hash": "d85ec7cb9b3b80783c8953553f7875d1b9d94eb55dcfd9a57b3ba357830c1934",
|
|
||||||
"runtime_path": "/home/lash/Desktop/projects/new_starr_app/node_modules/.pnpm/@rescript+runtime@12.3.0/node_modules/@rescript/runtime",
|
|
||||||
"generated_at": "1779801549320"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,58 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as JsxRuntime from "react/jsx-runtime";
|
|
||||||
|
|
||||||
function AnnotationPanel(props) {
|
|
||||||
let onClose = props.onClose;
|
|
||||||
let onNoteChange = props.onNoteChange;
|
|
||||||
let note = props.note;
|
|
||||||
let lesson = props.lesson;
|
|
||||||
let noteValue = note !== undefined ? note : "";
|
|
||||||
return JsxRuntime.jsxs("div", {
|
|
||||||
children: [
|
|
||||||
JsxRuntime.jsxs("div", {
|
|
||||||
children: [
|
|
||||||
JsxRuntime.jsxs("div", {
|
|
||||||
children: [
|
|
||||||
JsxRuntime.jsx("p", {
|
|
||||||
children: "Annotation",
|
|
||||||
className: "text-xs text-muted-foreground uppercase tracking-widest font-semibold"
|
|
||||||
}),
|
|
||||||
JsxRuntime.jsx("h2", {
|
|
||||||
children: lesson.title,
|
|
||||||
className: "text-lg font-bold mt-0.5"
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}),
|
|
||||||
JsxRuntime.jsx("button", {
|
|
||||||
children: "✕ Close",
|
|
||||||
className: "text-muted-foreground hover:text-foreground transition-colors text-sm",
|
|
||||||
onClick: param => onClose()
|
|
||||||
})
|
|
||||||
],
|
|
||||||
className: "flex items-center justify-between mb-4"
|
|
||||||
}),
|
|
||||||
JsxRuntime.jsx("textarea", {
|
|
||||||
className: "flex-1 w-full resize-none rounded-lg border bg-background p-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring",
|
|
||||||
placeholder: "Write your notes for this lesson...",
|
|
||||||
value: noteValue,
|
|
||||||
onChange: e => {
|
|
||||||
let value = e.target.value;
|
|
||||||
onNoteChange(lesson.id, value);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
JsxRuntime.jsx("p", {
|
|
||||||
children: "Auto-saved to your browser",
|
|
||||||
className: "text-xs text-muted-foreground mt-2"
|
|
||||||
})
|
|
||||||
],
|
|
||||||
className: "flex flex-col h-full"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let make = AnnotationPanel;
|
|
||||||
|
|
||||||
export {
|
|
||||||
make,
|
|
||||||
}
|
|
||||||
/* react/jsx-runtime Not a pure module */
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@react.component
|
|
||||||
let make = (
|
|
||||||
~lesson: AnnotationTypes.lesson,
|
|
||||||
~note: option<string>,
|
|
||||||
~onNoteChange: (string, string) => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => {
|
|
||||||
let noteValue = switch note {
|
|
||||||
| Some(n) => n
|
|
||||||
| None => ""
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-widest font-semibold">
|
|
||||||
{React.string("Annotation")}
|
|
||||||
</p>
|
|
||||||
<h2 className="text-lg font-bold mt-0.5">
|
|
||||||
{React.string(lesson.title)}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={_ => onClose()}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{React.string("✕ Close")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="flex-1 w-full resize-none rounded-lg border bg-background p-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
placeholder="Write your notes for this lesson..."
|
|
||||||
value={noteValue}
|
|
||||||
onChange={e => {
|
|
||||||
let value = ReactEvent.Form.target(e)["value"]
|
|
||||||
onNoteChange(lesson.id, value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
|
||||||
{React.string("Auto-saved to your browser")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,29 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
||||||
|
|
||||||
function storageKey(courseId, lessonId) {
|
|
||||||
return `annotation:` + courseId + `:` + lessonId;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveNote(courseId, lessonId, note) {
|
|
||||||
let key = storageKey(courseId, lessonId);
|
|
||||||
return localStorage.setItem(key, note);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadNote(courseId, lessonId) {
|
|
||||||
let key = storageKey(courseId, lessonId);
|
|
||||||
return Primitive_option.fromNullable(localStorage.getItem(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteNote(courseId, lessonId) {
|
|
||||||
return localStorage.removeItem(storageKey(courseId, lessonId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
storageKey,
|
|
||||||
saveNote,
|
|
||||||
loadNote,
|
|
||||||
deleteNote,
|
|
||||||
}
|
|
||||||
/* No side effect */
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
@val external localStorage: {..} = "localStorage"
|
|
||||||
|
|
||||||
let storageKey = (courseId: string, lessonId: string) =>
|
|
||||||
`annotation:${courseId}:${lessonId}`
|
|
||||||
|
|
||||||
let saveNote = (courseId: string, lessonId: string, note: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
localStorage["setItem"](key, note)
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadNote = (courseId: string, lessonId: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
let value: Nullable.t<string> = localStorage["getItem"](key)
|
|
||||||
Nullable.toOption(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleteNote = (courseId: string, lessonId: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
localStorage["removeItem"](key)
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,15 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
|
|
||||||
let initialState_notes = {};
|
|
||||||
|
|
||||||
let initialState = {
|
|
||||||
selectedLesson: undefined,
|
|
||||||
isPenOpen: false,
|
|
||||||
notes: initialState_notes
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
initialState,
|
|
||||||
}
|
|
||||||
/* No side effect */
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
type lesson = {
|
|
||||||
id: string,
|
|
||||||
title: string,
|
|
||||||
unitId: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type annotationState = {
|
|
||||||
selectedLesson: option<lesson>,
|
|
||||||
isPenOpen: bool,
|
|
||||||
notes: dict<string>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialState = {
|
|
||||||
selectedLesson: None,
|
|
||||||
isPenOpen: false,
|
|
||||||
notes: Dict.make(),
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as JsxRuntime from "react/jsx-runtime";
|
|
||||||
|
|
||||||
function Hello(props) {
|
|
||||||
return JsxRuntime.jsx("div", {
|
|
||||||
children: "ReScript is working!"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let make = Hello;
|
|
||||||
|
|
||||||
export {
|
|
||||||
make,
|
|
||||||
}
|
|
||||||
/* react/jsx-runtime Not a pure module */
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@react.component
|
|
||||||
let make = () => {
|
|
||||||
<div> {React.string("ReScript is working!")} </div>
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,192 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as UseSketch from "./useSketch.js";
|
|
||||||
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
||||||
import * as JsxRuntime from "react/jsx-runtime";
|
|
||||||
import SketchToolbarJsx from "./SketchToolbar.jsx";
|
|
||||||
|
|
||||||
let make = SketchToolbarJsx;
|
|
||||||
|
|
||||||
let SketchToolbar = {
|
|
||||||
make: make
|
|
||||||
};
|
|
||||||
|
|
||||||
function SketchCanvas(props) {
|
|
||||||
let onClear = props.onClear;
|
|
||||||
let onSave = props.onSave;
|
|
||||||
let color = props.color;
|
|
||||||
let tool = props.tool;
|
|
||||||
let isActive = props.isActive;
|
|
||||||
let canvasRef = React.useRef(null);
|
|
||||||
let isDrawing = React.useRef(false);
|
|
||||||
let lastPoint = React.useRef(undefined);
|
|
||||||
React.useEffect(() => {
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (!(el == null)) {
|
|
||||||
let parent = el.parentElement;
|
|
||||||
el.width = parent.offsetWidth;
|
|
||||||
el.height = parent.offsetHeight;
|
|
||||||
}
|
|
||||||
}, [isActive]);
|
|
||||||
let getPos = (e, canvas) => {
|
|
||||||
let rect = canvas.getBoundingClientRect();
|
|
||||||
return {
|
|
||||||
x: e.clientX - rect.left,
|
|
||||||
y: e.clientY - rect.top
|
|
||||||
};
|
|
||||||
};
|
|
||||||
let startDraw = e => {
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (el == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isDrawing.current = true;
|
|
||||||
let pos = getPos(e, el);
|
|
||||||
lastPoint.current = pos;
|
|
||||||
if (tool === "Highlighter") {
|
|
||||||
let ctx = el.getContext("2d");
|
|
||||||
let hex = UseSketch.colorToHex(color);
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0);
|
|
||||||
ctx.globalAlpha = 0.25;
|
|
||||||
ctx.globalCompositeOperation = "source-over";
|
|
||||||
ctx.strokeStyle = hex;
|
|
||||||
ctx.lineWidth = width;
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
ctx.lineJoin = "round";
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(pos.x, pos.y);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (tool !== "Text") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let text = el.ownerDocument.defaultView.prompt("Enter text:", "");
|
|
||||||
if (text == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ctx$1 = el.getContext("2d");
|
|
||||||
let hex$1 = UseSketch.colorToHex(color);
|
|
||||||
ctx$1.globalAlpha = 1.0;
|
|
||||||
ctx$1.fillStyle = hex$1;
|
|
||||||
ctx$1.font = "bold 16px sans-serif";
|
|
||||||
ctx$1.fillText(text, pos.x, pos.y);
|
|
||||||
};
|
|
||||||
let draw = e => {
|
|
||||||
if (!(isDrawing.current && tool !== "Text")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (el == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let last = lastPoint.current;
|
|
||||||
if (last === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ctx = el.getContext("2d");
|
|
||||||
let pos = getPos(e, el);
|
|
||||||
let hex = UseSketch.colorToHex(color);
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0);
|
|
||||||
let exit = 0;
|
|
||||||
switch (tool) {
|
|
||||||
case "Highlighter" :
|
|
||||||
ctx.lineTo(pos.x, pos.y);
|
|
||||||
ctx.stroke();
|
|
||||||
break;
|
|
||||||
case "Pen" :
|
|
||||||
case "Text" :
|
|
||||||
exit = 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (exit === 1) {
|
|
||||||
ctx.globalAlpha = 1.0;
|
|
||||||
ctx.strokeStyle = hex;
|
|
||||||
ctx.lineWidth = width;
|
|
||||||
ctx.lineCap = "round";
|
|
||||||
ctx.lineJoin = "round";
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(last.x, last.y);
|
|
||||||
ctx.quadraticCurveTo(last.x + (pos.x - last.x) / 2.0, last.y + (pos.y - last.y) / 2.0, pos.x, pos.y);
|
|
||||||
ctx.stroke();
|
|
||||||
}
|
|
||||||
lastPoint.current = pos;
|
|
||||||
};
|
|
||||||
let stopDraw = _e => {
|
|
||||||
if (tool === "Highlighter") {
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (!(el == null)) {
|
|
||||||
let ctx = el.getContext("2d");
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.globalAlpha = 1.0;
|
|
||||||
ctx.globalCompositeOperation = "source-over";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDrawing.current = false;
|
|
||||||
lastPoint.current = undefined;
|
|
||||||
};
|
|
||||||
let handleClear = () => {
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (el == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ctx = el.getContext("2d");
|
|
||||||
ctx.clearRect(0, 0, el.width, el.height);
|
|
||||||
onClear();
|
|
||||||
};
|
|
||||||
let handleSave = () => {
|
|
||||||
let el = canvasRef.current;
|
|
||||||
if (el == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let dataUrl = el.toDataURL("image/png");
|
|
||||||
let link = document.createElement("a");
|
|
||||||
link.href = dataUrl;
|
|
||||||
link.download = "sketch.png";
|
|
||||||
link.click();
|
|
||||||
onSave();
|
|
||||||
};
|
|
||||||
if (isActive) {
|
|
||||||
return JsxRuntime.jsxs("div", {
|
|
||||||
children: [
|
|
||||||
JsxRuntime.jsx(make, {
|
|
||||||
state: {
|
|
||||||
isActive: isActive,
|
|
||||||
tool: tool,
|
|
||||||
color: color,
|
|
||||||
strokeWidth: 2.0
|
|
||||||
},
|
|
||||||
onSetTool: props.onSetTool,
|
|
||||||
onSetColor: props.onSetColor,
|
|
||||||
onClear: handleClear,
|
|
||||||
onSave: handleSave,
|
|
||||||
onClose: props.onClose
|
|
||||||
}),
|
|
||||||
JsxRuntime.jsx("canvas", {
|
|
||||||
ref: Primitive_option.some(canvasRef),
|
|
||||||
className: "w-full h-full cursor-crosshair",
|
|
||||||
height: "100%",
|
|
||||||
width: "100%",
|
|
||||||
onMouseDown: startDraw,
|
|
||||||
onMouseLeave: stopDraw,
|
|
||||||
onMouseMove: draw,
|
|
||||||
onMouseUp: stopDraw
|
|
||||||
})
|
|
||||||
],
|
|
||||||
className: "absolute inset-0 z-10",
|
|
||||||
style: {
|
|
||||||
pointerEvents: isActive ? "all" : "none"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let make$1 = SketchCanvas;
|
|
||||||
|
|
||||||
export {
|
|
||||||
SketchToolbar,
|
|
||||||
make$1 as make,
|
|
||||||
}
|
|
||||||
/* make Not a pure module */
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
module SketchToolbar = {
|
|
||||||
@module("./SketchToolbar.jsx") @react.component
|
|
||||||
external make: (
|
|
||||||
~state: UseSketch.sketchState,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => React.element = "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
type point = {x: float, y: float}
|
|
||||||
|
|
||||||
@val external document: {..} = "document"
|
|
||||||
external asCanvas: Dom.element => {..} = "%identity"
|
|
||||||
|
|
||||||
@react.component
|
|
||||||
let make = (
|
|
||||||
~isActive: bool,
|
|
||||||
~tool: UseSketch.tool,
|
|
||||||
~color: UseSketch.color,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
) => {
|
|
||||||
let canvasRef: React.ref<Nullable.t<Dom.element>> = React.useRef(Nullable.null)
|
|
||||||
let isDrawing = React.useRef(false)
|
|
||||||
let lastPoint = React.useRef(None)
|
|
||||||
|
|
||||||
React.useEffect1(() => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let parent = canvas["parentElement"]
|
|
||||||
canvas["width"] = parent["offsetWidth"]
|
|
||||||
canvas["height"] = parent["offsetHeight"]
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}, [isActive])
|
|
||||||
|
|
||||||
let getPos = (e: JsxEvent.Mouse.t, canvas) => {
|
|
||||||
let rect = canvas["getBoundingClientRect"]()
|
|
||||||
{
|
|
||||||
x: Float.fromInt(JsxEvent.Mouse.clientX(e)) -. rect["left"],
|
|
||||||
y: Float.fromInt(JsxEvent.Mouse.clientY(e)) -. rect["top"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let startDraw = (e: JsxEvent.Mouse.t) => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
isDrawing.current = true
|
|
||||||
let pos = getPos(e, canvas)
|
|
||||||
lastPoint.current = Some(pos)
|
|
||||||
if tool === UseSketch.Highlighter {
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
|
||||||
ctx["globalAlpha"] = 0.25
|
|
||||||
ctx["globalCompositeOperation"] = "source-over"
|
|
||||||
ctx["strokeStyle"] = hex
|
|
||||||
ctx["lineWidth"] = width
|
|
||||||
ctx["lineCap"] = "round"
|
|
||||||
ctx["lineJoin"] = "round"
|
|
||||||
let _ = ctx["beginPath"]()
|
|
||||||
let _ = ctx["moveTo"](pos.x, pos.y)
|
|
||||||
} else if tool === UseSketch.Text {
|
|
||||||
let text = canvas["ownerDocument"]["defaultView"]["prompt"]("Enter text:", "")
|
|
||||||
switch Nullable.toOption(text) {
|
|
||||||
| None => ()
|
|
||||||
| Some(t) =>
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["fillStyle"] = hex
|
|
||||||
ctx["font"] = "bold 16px sans-serif"
|
|
||||||
let _ = ctx["fillText"](t, pos.x, pos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let draw = (e: JsxEvent.Mouse.t) => {
|
|
||||||
if isDrawing.current && tool !== UseSketch.Text {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
switch lastPoint.current {
|
|
||||||
| None => ()
|
|
||||||
| Some(last) =>
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let pos = getPos(e, canvas)
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
|
||||||
switch tool {
|
|
||||||
| UseSketch.Highlighter =>
|
|
||||||
let _ = ctx["lineTo"](pos.x, pos.y)
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
| _ =>
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["strokeStyle"] = hex
|
|
||||||
ctx["lineWidth"] = width
|
|
||||||
ctx["lineCap"] = "round"
|
|
||||||
ctx["lineJoin"] = "round"
|
|
||||||
let _ = ctx["beginPath"]()
|
|
||||||
let _ = ctx["moveTo"](last.x, last.y)
|
|
||||||
let _ = ctx["quadraticCurveTo"](
|
|
||||||
last.x +. (pos.x -. last.x) /. 2.0,
|
|
||||||
last.y +. (pos.y -. last.y) /. 2.0,
|
|
||||||
pos.x,
|
|
||||||
pos.y,
|
|
||||||
)
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
}
|
|
||||||
lastPoint.current = Some(pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let stopDraw = (_e: JsxEvent.Mouse.t) => {
|
|
||||||
if tool === UseSketch.Highlighter {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["globalCompositeOperation"] = "source-over"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDrawing.current = false
|
|
||||||
lastPoint.current = None
|
|
||||||
}
|
|
||||||
|
|
||||||
let handleClear = () => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let _ = ctx["clearRect"](0, 0, canvas["width"], canvas["height"])
|
|
||||||
onClear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handleSave = () => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let dataUrl = canvas["toDataURL"]("image/png")
|
|
||||||
let link = document["createElement"]("a")
|
|
||||||
link["href"] = dataUrl
|
|
||||||
link["download"] = "sketch.png"
|
|
||||||
let _ = link["click"]()
|
|
||||||
onSave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isActive {
|
|
||||||
React.null
|
|
||||||
} else {
|
|
||||||
<div className="absolute inset-0 z-10" style={{pointerEvents: isActive ? "all" : "none"}}>
|
|
||||||
<SketchToolbar.make
|
|
||||||
state={{
|
|
||||||
UseSketch.isActive,
|
|
||||||
tool,
|
|
||||||
color,
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
}}
|
|
||||||
onSetTool={onSetTool}
|
|
||||||
onSetColor={onSetColor}
|
|
||||||
onClear={handleClear}
|
|
||||||
onSave={handleSave}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef->ReactDOM.Ref.domRef}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
className="w-full h-full cursor-crosshair"
|
|
||||||
onMouseDown={startDraw}
|
|
||||||
onMouseMove={draw}
|
|
||||||
onMouseUp={stopDraw}
|
|
||||||
onMouseLeave={stopDraw}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,14 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import SketchToolbarJsx from "./SketchToolbar.jsx";
|
|
||||||
|
|
||||||
let make = SketchToolbarJsx;
|
|
||||||
|
|
||||||
let SketchToolbar = {
|
|
||||||
make: make
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
SketchToolbar,
|
|
||||||
}
|
|
||||||
/* make Not a pure module */
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
module SketchToolbar = {
|
|
||||||
@module("./SketchToolbar.jsx") @react.component
|
|
||||||
external make: (
|
|
||||||
~state: UseSketch.sketchState,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => React.element = "default"
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,106 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
let initialState = {
|
|
||||||
isActive: false,
|
|
||||||
tool: "Pen",
|
|
||||||
color: "Black",
|
|
||||||
strokeWidth: 2.0
|
|
||||||
};
|
|
||||||
|
|
||||||
function colorToHex(color) {
|
|
||||||
switch (color) {
|
|
||||||
case "Black" :
|
|
||||||
return "#000000";
|
|
||||||
case "Red" :
|
|
||||||
return "#ef4444";
|
|
||||||
case "Blue" :
|
|
||||||
return "#3b82f6";
|
|
||||||
case "Yellow" :
|
|
||||||
return "#eab308";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolToOpacity(tool) {
|
|
||||||
switch (tool) {
|
|
||||||
case "Highlighter" :
|
|
||||||
return 0.3;
|
|
||||||
case "Pen" :
|
|
||||||
case "Text" :
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHighlighter(tool) {
|
|
||||||
switch (tool) {
|
|
||||||
case "Highlighter" :
|
|
||||||
return true;
|
|
||||||
case "Pen" :
|
|
||||||
case "Text" :
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toolToWidth(tool, base) {
|
|
||||||
switch (tool) {
|
|
||||||
case "Highlighter" :
|
|
||||||
return base * 8.0;
|
|
||||||
case "Pen" :
|
|
||||||
case "Text" :
|
|
||||||
return base;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function use() {
|
|
||||||
let match = React.useState(() => initialState);
|
|
||||||
let setState = match[1];
|
|
||||||
let activate = () => setState(prev => ({
|
|
||||||
isActive: true,
|
|
||||||
tool: prev.tool,
|
|
||||||
color: prev.color,
|
|
||||||
strokeWidth: prev.strokeWidth
|
|
||||||
}));
|
|
||||||
let deactivate = () => setState(prev => ({
|
|
||||||
isActive: false,
|
|
||||||
tool: prev.tool,
|
|
||||||
color: prev.color,
|
|
||||||
strokeWidth: prev.strokeWidth
|
|
||||||
}));
|
|
||||||
let toggle = () => setState(prev => ({
|
|
||||||
isActive: !prev.isActive,
|
|
||||||
tool: prev.tool,
|
|
||||||
color: prev.color,
|
|
||||||
strokeWidth: prev.strokeWidth
|
|
||||||
}));
|
|
||||||
let setTool = tool => setState(prev => ({
|
|
||||||
isActive: prev.isActive,
|
|
||||||
tool: tool,
|
|
||||||
color: prev.color,
|
|
||||||
strokeWidth: prev.strokeWidth
|
|
||||||
}));
|
|
||||||
let setColor = color => setState(prev => ({
|
|
||||||
isActive: prev.isActive,
|
|
||||||
tool: prev.tool,
|
|
||||||
color: color,
|
|
||||||
strokeWidth: prev.strokeWidth
|
|
||||||
}));
|
|
||||||
return [
|
|
||||||
match[0],
|
|
||||||
activate,
|
|
||||||
deactivate,
|
|
||||||
toggle,
|
|
||||||
setTool,
|
|
||||||
setColor
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
initialState,
|
|
||||||
colorToHex,
|
|
||||||
toolToOpacity,
|
|
||||||
isHighlighter,
|
|
||||||
toolToWidth,
|
|
||||||
use,
|
|
||||||
}
|
|
||||||
/* react Not a pure module */
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
type tool =
|
|
||||||
| Pen
|
|
||||||
| Highlighter
|
|
||||||
| Text
|
|
||||||
|
|
||||||
type color =
|
|
||||||
| Black
|
|
||||||
| Red
|
|
||||||
| Blue
|
|
||||||
| Yellow
|
|
||||||
|
|
||||||
type sketchState = {
|
|
||||||
isActive: bool,
|
|
||||||
tool: tool,
|
|
||||||
color: color,
|
|
||||||
strokeWidth: float,
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialState: sketchState = {
|
|
||||||
isActive: false,
|
|
||||||
tool: Pen,
|
|
||||||
color: Black,
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
let colorToHex = (color: color) =>
|
|
||||||
switch color {
|
|
||||||
| Black => "#000000"
|
|
||||||
| Red => "#ef4444"
|
|
||||||
| Blue => "#3b82f6"
|
|
||||||
| Yellow => "#eab308"
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolToOpacity = (tool: tool) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => 0.3
|
|
||||||
| Pen | Text => 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
let isHighlighter = (tool: tool) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => true
|
|
||||||
| Pen | Text => false
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolToWidth = (tool: tool, base: float) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => base *. 8.0
|
|
||||||
| Pen => base
|
|
||||||
| Text => base
|
|
||||||
}
|
|
||||||
|
|
||||||
let use = () => {
|
|
||||||
let (state, setState) = React.useState(() => initialState)
|
|
||||||
|
|
||||||
let activate = () => setState(prev => {...prev, isActive: true})
|
|
||||||
|
|
||||||
let deactivate = () => setState(prev => {...prev, isActive: false})
|
|
||||||
|
|
||||||
let toggle = () => setState(prev => {...prev, isActive: !prev.isActive})
|
|
||||||
|
|
||||||
let setTool = (tool: tool) => setState(prev => {...prev, tool})
|
|
||||||
|
|
||||||
let setColor = (color: color) => setState(prev => {...prev, color})
|
|
||||||
|
|
||||||
(state, activate, deactivate, toggle, setTool, setColor)
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,53 +0,0 @@
|
|||||||
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as AnnotationTypes from "./AnnotationTypes.js";
|
|
||||||
import * as AnnotationStorage from "./AnnotationStorage.js";
|
|
||||||
|
|
||||||
function use(courseId) {
|
|
||||||
let match = React.useState(() => AnnotationTypes.initialState);
|
|
||||||
let setState = match[1];
|
|
||||||
let state = match[0];
|
|
||||||
let selectLesson = lesson => {
|
|
||||||
let savedNote = AnnotationStorage.loadNote(courseId, lesson.id);
|
|
||||||
let notes = {};
|
|
||||||
if (savedNote !== undefined) {
|
|
||||||
notes[lesson.id] = savedNote;
|
|
||||||
}
|
|
||||||
setState(param => ({
|
|
||||||
selectedLesson: lesson,
|
|
||||||
isPenOpen: true,
|
|
||||||
notes: notes
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
let updateNote = (lessonId, note) => {
|
|
||||||
AnnotationStorage.saveNote(courseId, lessonId, note);
|
|
||||||
setState(prev => {
|
|
||||||
let notes = {};
|
|
||||||
notes[lessonId] = note;
|
|
||||||
return {
|
|
||||||
selectedLesson: prev.selectedLesson,
|
|
||||||
isPenOpen: prev.isPenOpen,
|
|
||||||
notes: notes
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let closePen = () => setState(prev => ({
|
|
||||||
selectedLesson: undefined,
|
|
||||||
isPenOpen: false,
|
|
||||||
notes: prev.notes
|
|
||||||
}));
|
|
||||||
let getNote = lessonId => state.notes[lessonId];
|
|
||||||
return [
|
|
||||||
state,
|
|
||||||
selectLesson,
|
|
||||||
updateNote,
|
|
||||||
closePen,
|
|
||||||
getNote
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
use,
|
|
||||||
}
|
|
||||||
/* react Not a pure module */
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
@val external window: {..} = "window"
|
|
||||||
|
|
||||||
let use = (courseId: string) => {
|
|
||||||
let (state, setState) = React.useState(() => AnnotationTypes.initialState)
|
|
||||||
|
|
||||||
let selectLesson = (lesson: AnnotationTypes.lesson) => {
|
|
||||||
let savedNote = AnnotationStorage.loadNote(courseId, lesson.id)
|
|
||||||
let notes = Dict.make()
|
|
||||||
switch savedNote {
|
|
||||||
| Some(note) => Dict.set(notes, lesson.id, note)
|
|
||||||
| None => ()
|
|
||||||
}
|
|
||||||
setState(_ => {
|
|
||||||
selectedLesson: Some(lesson),
|
|
||||||
isPenOpen: true,
|
|
||||||
notes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateNote = (lessonId: string, note: string) => {
|
|
||||||
AnnotationStorage.saveNote(courseId, lessonId, note)
|
|
||||||
setState(prev => {
|
|
||||||
let notes = Dict.make()
|
|
||||||
Dict.set(notes, lessonId, note)
|
|
||||||
{...prev, notes}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let closePen = () => {
|
|
||||||
setState(prev => {...prev, isPenOpen: false, selectedLesson: None})
|
|
||||||
}
|
|
||||||
|
|
||||||
let getNote = (lessonId: string) => {
|
|
||||||
Dict.get(state.notes, lessonId)
|
|
||||||
}
|
|
||||||
|
|
||||||
(state, selectLesson, updateNote, closePen, getNote)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
@react.component
|
|
||||||
let make = (
|
|
||||||
~lesson: AnnotationTypes.lesson,
|
|
||||||
~note: option<string>,
|
|
||||||
~onNoteChange: (string, string) => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => {
|
|
||||||
let noteValue = switch note {
|
|
||||||
| Some(n) => n
|
|
||||||
| None => ""
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className="flex flex-col h-full">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<div>
|
|
||||||
<p className="text-xs text-muted-foreground uppercase tracking-widest font-semibold">
|
|
||||||
{React.string("Annotation")}
|
|
||||||
</p>
|
|
||||||
<h2 className="text-lg font-bold mt-0.5">
|
|
||||||
{React.string(lesson.title)}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={_ => onClose()}
|
|
||||||
className="text-muted-foreground hover:text-foreground transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{React.string("✕ Close")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="flex-1 w-full resize-none rounded-lg border bg-background p-4 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
placeholder="Write your notes for this lesson..."
|
|
||||||
value={noteValue}
|
|
||||||
onChange={e => {
|
|
||||||
let value = ReactEvent.Form.target(e)["value"]
|
|
||||||
onNoteChange(lesson.id, value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-muted-foreground mt-2">
|
|
||||||
{React.string("Auto-saved to your browser")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,20 +0,0 @@
|
|||||||
@val external localStorage: {..} = "localStorage"
|
|
||||||
|
|
||||||
let storageKey = (courseId: string, lessonId: string) =>
|
|
||||||
`annotation:${courseId}:${lessonId}`
|
|
||||||
|
|
||||||
let saveNote = (courseId: string, lessonId: string, note: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
localStorage["setItem"](key, note)
|
|
||||||
}
|
|
||||||
|
|
||||||
let loadNote = (courseId: string, lessonId: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
let value: Nullable.t<string> = localStorage["getItem"](key)
|
|
||||||
Nullable.toOption(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
let deleteNote = (courseId: string, lessonId: string) => {
|
|
||||||
let key = storageKey(courseId, lessonId)
|
|
||||||
localStorage["removeItem"](key)
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,17 +0,0 @@
|
|||||||
type lesson = {
|
|
||||||
id: string,
|
|
||||||
title: string,
|
|
||||||
unitId: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
type annotationState = {
|
|
||||||
selectedLesson: option<lesson>,
|
|
||||||
isPenOpen: bool,
|
|
||||||
notes: dict<string>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialState = {
|
|
||||||
selectedLesson: None,
|
|
||||||
isPenOpen: false,
|
|
||||||
notes: Dict.make(),
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@react.component
|
|
||||||
let make = () => {
|
|
||||||
<div> {React.string("ReScript is working!")} </div>
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,198 +0,0 @@
|
|||||||
module SketchToolbar = {
|
|
||||||
@module("./SketchToolbar.jsx") @react.component
|
|
||||||
external make: (
|
|
||||||
~state: UseSketch.sketchState,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => React.element = "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
type point = {x: float, y: float}
|
|
||||||
|
|
||||||
@val external document: {..} = "document"
|
|
||||||
external asCanvas: Dom.element => {..} = "%identity"
|
|
||||||
|
|
||||||
@react.component
|
|
||||||
let make = (
|
|
||||||
~isActive: bool,
|
|
||||||
~tool: UseSketch.tool,
|
|
||||||
~color: UseSketch.color,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
) => {
|
|
||||||
let canvasRef: React.ref<Nullable.t<Dom.element>> = React.useRef(Nullable.null)
|
|
||||||
let isDrawing = React.useRef(false)
|
|
||||||
let lastPoint = React.useRef(None)
|
|
||||||
|
|
||||||
React.useEffect1(() => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let parent = canvas["parentElement"]
|
|
||||||
canvas["width"] = parent["offsetWidth"]
|
|
||||||
canvas["height"] = parent["offsetHeight"]
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}, [isActive])
|
|
||||||
|
|
||||||
let getPos = (e: JsxEvent.Mouse.t, canvas) => {
|
|
||||||
let rect = canvas["getBoundingClientRect"]()
|
|
||||||
{
|
|
||||||
x: Float.fromInt(JsxEvent.Mouse.clientX(e)) -. rect["left"],
|
|
||||||
y: Float.fromInt(JsxEvent.Mouse.clientY(e)) -. rect["top"],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let startDraw = (e: JsxEvent.Mouse.t) => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
isDrawing.current = true
|
|
||||||
let pos = getPos(e, canvas)
|
|
||||||
lastPoint.current = Some(pos)
|
|
||||||
if tool === UseSketch.Highlighter {
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
|
||||||
ctx["globalAlpha"] = 0.25
|
|
||||||
ctx["globalCompositeOperation"] = "source-over"
|
|
||||||
ctx["strokeStyle"] = hex
|
|
||||||
ctx["lineWidth"] = width
|
|
||||||
ctx["lineCap"] = "round"
|
|
||||||
ctx["lineJoin"] = "round"
|
|
||||||
let _ = ctx["beginPath"]()
|
|
||||||
let _ = ctx["moveTo"](pos.x, pos.y)
|
|
||||||
} else if tool === UseSketch.Text {
|
|
||||||
let text = canvas["ownerDocument"]["defaultView"]["prompt"]("Enter text:", "")
|
|
||||||
switch Nullable.toOption(text) {
|
|
||||||
| None => ()
|
|
||||||
| Some(t) =>
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["fillStyle"] = hex
|
|
||||||
ctx["font"] = "bold 16px sans-serif"
|
|
||||||
let _ = ctx["fillText"](t, pos.x, pos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let draw = (e: JsxEvent.Mouse.t) => {
|
|
||||||
if isDrawing.current && tool !== UseSketch.Text {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
switch lastPoint.current {
|
|
||||||
| None => ()
|
|
||||||
| Some(last) =>
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let pos = getPos(e, canvas)
|
|
||||||
let hex = UseSketch.colorToHex(color)
|
|
||||||
let width = UseSketch.toolToWidth(tool, 2.0)
|
|
||||||
switch tool {
|
|
||||||
| UseSketch.Highlighter =>
|
|
||||||
let _ = ctx["lineTo"](pos.x, pos.y)
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
| _ =>
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["strokeStyle"] = hex
|
|
||||||
ctx["lineWidth"] = width
|
|
||||||
ctx["lineCap"] = "round"
|
|
||||||
ctx["lineJoin"] = "round"
|
|
||||||
let _ = ctx["beginPath"]()
|
|
||||||
let _ = ctx["moveTo"](last.x, last.y)
|
|
||||||
let _ = ctx["quadraticCurveTo"](
|
|
||||||
last.x +. (pos.x -. last.x) /. 2.0,
|
|
||||||
last.y +. (pos.y -. last.y) /. 2.0,
|
|
||||||
pos.x,
|
|
||||||
pos.y,
|
|
||||||
)
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
}
|
|
||||||
lastPoint.current = Some(pos)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let stopDraw = (_e: JsxEvent.Mouse.t) => {
|
|
||||||
if tool === UseSketch.Highlighter {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let _ = ctx["stroke"]()
|
|
||||||
ctx["globalAlpha"] = 1.0
|
|
||||||
ctx["globalCompositeOperation"] = "source-over"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isDrawing.current = false
|
|
||||||
lastPoint.current = None
|
|
||||||
}
|
|
||||||
|
|
||||||
let handleClear = () => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let ctx = canvas["getContext"]("2d")
|
|
||||||
let _ = ctx["clearRect"](0, 0, canvas["width"], canvas["height"])
|
|
||||||
onClear()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let handleSave = () => {
|
|
||||||
switch canvasRef.current->Nullable.toOption {
|
|
||||||
| None => ()
|
|
||||||
| Some(el) =>
|
|
||||||
let canvas = asCanvas(el)
|
|
||||||
let dataUrl = canvas["toDataURL"]("image/png")
|
|
||||||
let link = document["createElement"]("a")
|
|
||||||
link["href"] = dataUrl
|
|
||||||
link["download"] = "sketch.png"
|
|
||||||
let _ = link["click"]()
|
|
||||||
onSave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isActive {
|
|
||||||
React.null
|
|
||||||
} else {
|
|
||||||
<div className="absolute inset-0 z-10" style={{pointerEvents: isActive ? "all" : "none"}}>
|
|
||||||
<SketchToolbar.make
|
|
||||||
state={{
|
|
||||||
UseSketch.isActive,
|
|
||||||
tool,
|
|
||||||
color,
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
}}
|
|
||||||
onSetTool={onSetTool}
|
|
||||||
onSetColor={onSetColor}
|
|
||||||
onClear={handleClear}
|
|
||||||
onSave={handleSave}
|
|
||||||
onClose={onClose}
|
|
||||||
/>
|
|
||||||
<canvas
|
|
||||||
ref={canvasRef->ReactDOM.Ref.domRef}
|
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
className="w-full h-full cursor-crosshair"
|
|
||||||
onMouseDown={startDraw}
|
|
||||||
onMouseMove={draw}
|
|
||||||
onMouseUp={stopDraw}
|
|
||||||
onMouseLeave={stopDraw}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,11 +0,0 @@
|
|||||||
module SketchToolbar = {
|
|
||||||
@module("./SketchToolbar.jsx") @react.component
|
|
||||||
external make: (
|
|
||||||
~state: UseSketch.sketchState,
|
|
||||||
~onSetTool: UseSketch.tool => unit,
|
|
||||||
~onSetColor: UseSketch.color => unit,
|
|
||||||
~onClear: unit => unit,
|
|
||||||
~onSave: unit => unit,
|
|
||||||
~onClose: unit => unit,
|
|
||||||
) => React.element = "default"
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
@val external window: {..} = "window"
|
|
||||||
|
|
||||||
let use = (courseId: string) => {
|
|
||||||
let (state, setState) = React.useState(() => AnnotationTypes.initialState)
|
|
||||||
|
|
||||||
let selectLesson = (lesson: AnnotationTypes.lesson) => {
|
|
||||||
let savedNote = AnnotationStorage.loadNote(courseId, lesson.id)
|
|
||||||
let notes = Dict.make()
|
|
||||||
switch savedNote {
|
|
||||||
| Some(note) => Dict.set(notes, lesson.id, note)
|
|
||||||
| None => ()
|
|
||||||
}
|
|
||||||
setState(_ => {
|
|
||||||
selectedLesson: Some(lesson),
|
|
||||||
isPenOpen: true,
|
|
||||||
notes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let updateNote = (lessonId: string, note: string) => {
|
|
||||||
AnnotationStorage.saveNote(courseId, lessonId, note)
|
|
||||||
setState(prev => {
|
|
||||||
let notes = Dict.make()
|
|
||||||
Dict.set(notes, lessonId, note)
|
|
||||||
{...prev, notes}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let closePen = () => {
|
|
||||||
setState(prev => {...prev, isPenOpen: false, selectedLesson: None})
|
|
||||||
}
|
|
||||||
|
|
||||||
let getNote = (lessonId: string) => {
|
|
||||||
Dict.get(state.notes, lessonId)
|
|
||||||
}
|
|
||||||
|
|
||||||
(state, selectLesson, updateNote, closePen, getNote)
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,67 +0,0 @@
|
|||||||
type tool =
|
|
||||||
| Pen
|
|
||||||
| Highlighter
|
|
||||||
| Text
|
|
||||||
|
|
||||||
type color =
|
|
||||||
| Black
|
|
||||||
| Red
|
|
||||||
| Blue
|
|
||||||
| Yellow
|
|
||||||
|
|
||||||
type sketchState = {
|
|
||||||
isActive: bool,
|
|
||||||
tool: tool,
|
|
||||||
color: color,
|
|
||||||
strokeWidth: float,
|
|
||||||
}
|
|
||||||
|
|
||||||
let initialState: sketchState = {
|
|
||||||
isActive: false,
|
|
||||||
tool: Pen,
|
|
||||||
color: Black,
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
let colorToHex = (color: color) =>
|
|
||||||
switch color {
|
|
||||||
| Black => "#000000"
|
|
||||||
| Red => "#ef4444"
|
|
||||||
| Blue => "#3b82f6"
|
|
||||||
| Yellow => "#eab308"
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolToOpacity = (tool: tool) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => 0.3
|
|
||||||
| Pen | Text => 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
let isHighlighter = (tool: tool) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => true
|
|
||||||
| Pen | Text => false
|
|
||||||
}
|
|
||||||
|
|
||||||
let toolToWidth = (tool: tool, base: float) =>
|
|
||||||
switch tool {
|
|
||||||
| Highlighter => base *. 8.0
|
|
||||||
| Pen => base
|
|
||||||
| Text => base
|
|
||||||
}
|
|
||||||
|
|
||||||
let use = () => {
|
|
||||||
let (state, setState) = React.useState(() => initialState)
|
|
||||||
|
|
||||||
let activate = () => setState(prev => {...prev, isActive: true})
|
|
||||||
|
|
||||||
let deactivate = () => setState(prev => {...prev, isActive: false})
|
|
||||||
|
|
||||||
let toggle = () => setState(prev => {...prev, isActive: !prev.isActive})
|
|
||||||
|
|
||||||
let setTool = (tool: tool) => setState(prev => {...prev, tool})
|
|
||||||
|
|
||||||
let setColor = (color: color) => setState(prev => {...prev, color})
|
|
||||||
|
|
||||||
(state, activate, deactivate, toggle, setTool, setColor)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user