| import gleam/list |
| import gleam/float |
| import gleam/int |
|
|
| pub type SketchStroke { |
| SketchStroke( |
| points: List(#(Float, Float)), |
| color: String, |
| thickness: Float, |
| timestamp: String, |
| ) |
| } |
|
|
| pub type GhostWritingSession { |
| GhostWritingSession( |
| session_id: String, |
| reference_image: String, |
| user_strokes: List(SketchStroke), |
| ghost_image_opacity: Float, |
| current_page: Int, |
| total_pages: Int, |
| ) |
| } |
|
|
| pub type SimilarityScore { |
| SimilarityScore( |
| overall_score: Float, |
| structural_accuracy: Float, |
| proportional_accuracy: Float, |
| detail_completeness: Float, |
| feedback: List(String), |
| ) |
| } |
|
|
| pub fn start_ghost_writing_session( |
| image_url: String, |
| ) -> GhostWritingSession { |
| GhostWritingSession( |
| session_id: "SESSION_2026", |
| reference_image: image_url, |
| user_strokes: [], |
| ghost_image_opacity: 0.3, |
| current_page: 1, |
| total_pages: 1, |
| ) |
| } |
|
|
| pub fn create_stroke( |
| points: List(#(Float, Float)), |
| color: String, |
| thickness: Float, |
| ) -> SketchStroke { |
| SketchStroke( |
| points: points, |
| color: color, |
| thickness: thickness, |
| timestamp: "2026-03-19T00:00:00Z", |
| ) |
| } |
|
|
| pub fn add_stroke_to_session( |
| session: GhostWritingSession, |
| stroke: SketchStroke, |
| ) -> GhostWritingSession { |
| GhostWritingSession( |
| ..session, |
| user_strokes: [stroke, ..session.user_strokes], |
| ) |
| } |
|
|
| pub fn adjust_ghost_opacity( |
| session: GhostWritingSession, |
| opacity: Float, |
| ) -> GhostWritingSession { |
| let clamped_opacity = case opacity { |
| o if o <. 0.0 -> 0.0 |
| o if o >. 1.0 -> 1.0 |
| o -> o |
| } |
| GhostWritingSession(..session, ghost_image_opacity: clamped_opacity) |
| } |
|
|
| pub fn calculate_similarity( |
| user_drawing: List(SketchStroke), |
| _reference_image: String, |
| ) -> SimilarityScore { |
| let stroke_count = list.length(user_drawing) |
|
|
| let structural_accuracy = case stroke_count { |
| c if c > 15 -> 85.0 |
| c if c > 8 -> 65.0 |
| c if c > 3 -> 45.0 |
| _ -> 20.0 |
| } |
|
|
| let proportional_accuracy = case stroke_count { |
| c if c > 10 -> 80.0 |
| _ -> 50.0 |
| } |
|
|
| let detail_completeness = case stroke_count { |
| c if c > 15 -> 90.0 |
| c if c > 8 -> 70.0 |
| _ -> 40.0 |
| } |
|
|
| let overall = |
| {structural_accuracy +. proportional_accuracy +. detail_completeness} /. 3.0 |
|
|
| SimilarityScore( |
| overall_score: overall, |
| structural_accuracy: structural_accuracy, |
| proportional_accuracy: proportional_accuracy, |
| detail_completeness: detail_completeness, |
| feedback: generate_drawing_feedback(overall, stroke_count), |
| ) |
| } |
|
|
| fn generate_drawing_feedback(score: Float, stroke_count: Int) -> List(String) { |
| let base_feedback = case score { |
| s if s >. 85.0 -> [ |
| "🏆 Exceptional job! Your drawing matches the reference perfectly!", |
| "Your spatial understanding is excellent.", |
| ] |
| s if s >. 70.0 -> [ |
| "💪 Great work! Your sketch is very accurate.", |
| "Minor details could be refined.", |
| ] |
| s if s >. 50.0 -> [ |
| "📈 Good effort! The main structure is recognizable.", |
| "Try adding more detail.", |
| ] |
| _ -> [ |
| "Keep practicing! Reference the ghost image more carefully.", |
| "Add more strokes to complete the drawing.", |
| ] |
| } |
|
|
| case stroke_count { |
| c if c < 3 -> |
| list.append(base_feedback, ["Remember: Draw more strokes for better results!"]) |
| _ -> base_feedback |
| } |
| } |
|
|
| pub fn generate_comparison_visualization( |
| user_drawing: List(SketchStroke), |
| _reference_image: String, |
| ) -> String { |
| let canvas = "<canvas id='comparison' width='800' height='600'></canvas>" |
| let user_svg = render_strokes(user_drawing) |
| "<div class='ghost-comparison'>" <> canvas <> user_svg <> "</div>" |
| } |
|
|
| fn render_strokes(strokes: List(SketchStroke)) -> String { |
| "<svg class='user-drawing' width='800' height='600'>" <> |
| list.fold(strokes, "", fn(acc, stroke) { |
| acc <> render_stroke(stroke) |
| }) <> |
| "</svg>" |
| } |
|
|
| fn render_stroke(stroke: SketchStroke) -> String { |
| case stroke.points { |
| [] -> "" |
| [first, ..rest] -> { |
| let #(x, y) = first |
| let path_start = |
| "M " <> float.to_string(x) <> " " <> float.to_string(y) |
| let path_rest = |
| list.fold(rest, "", fn(acc, point) { |
| let #(px, py) = point |
| acc <> " L " <> float.to_string(px) <> " " <> float.to_string(py) |
| }) |
| "<path d='" <> |
| path_start <> |
| path_rest <> |
| "' stroke='" <> |
| stroke.color <> |
| "' stroke-width='" <> |
| float.to_string(stroke.thickness) <> |
| "' fill='none' stroke-linecap='round' />" |
| } |
| } |
| } |
|
|
| pub fn export_drawing_as_image(strokes: List(SketchStroke)) -> String { |
| // Would generate PNG in production |
| render_strokes(strokes) |
| } |
|
|
| pub fn compare_drawings( |
| drawing1: List(SketchStroke), |
| drawing2: List(SketchStroke), |
| ) -> Float { |
| // Simplified similarity metric |
| let denominator = |
| int.max( |
| list.length(drawing1), |
| list.length(drawing2), |
| ) |
|
|
| case denominator == 0 { |
| True -> 0.0 |
| False -> |
| int.to_float( |
| int.min( |
| list.length(drawing1), |
| list.length(drawing2), |
| ), |
| ) /. int.to_float(denominator) *. 100.0 |
| } |
| } |
|
|
| pub fn suggest_next_diagram( |
| user_level: Float, |
| ) -> String { |
| case user_level { |
| l if l >. 85.0 -> |
| "Next: Advanced Anatomy (Heart Cross-Section)" |
| l if l >. 70.0 -> |
| "Next: Intermediate Diagram (Chemical Structure)" |
| l if l >. 50.0 -> |
| "Next: Basic Sketch (Simple Geometric Shape)" |
| _ -> |
| "Practice more with simpler diagrams first." |
| } |
| } |
|
|
| pub fn track_drawing_history(session: GhostWritingSession) -> String { |
| let stroke_count = list.length(session.user_strokes) |
| "Drawing Progress: " <> |
| int.to_string(stroke_count) <> |
| " strokes | Page " <> |
| int.to_string(session.current_page) <> |
| "/" <> |
| int.to_string(session.total_pages) |
| } |
|
|
| pub fn create_drawing_canvas_html() -> String { |
| " |
| <div class='ghost-writing-container'> |
| <div class='ghost-layer'> |
| <img id='ghostImg' src='' alt='Reference' class='ghost-image' /> |
| <canvas id='drawingCanvas' width='800' height='600' class='drawing-canvas'></canvas> |
| </div> |
| <div class='controls'> |
| <label>Ghost Opacity: <input type='range' id='opacitySlider' min='0' max='100' value='30'></label> |
| <button onclick='undoStroke()'>Undo</button> |
| <button onclick='clearDrawing()'>Clear</button> |
| <button onclick='submitDrawing()'>Submit</button> |
| </div> |
| <div id='feedback' class='feedback'></div> |
| </div>" |
| } |
|
|
| pub fn analyze_drawing_curves(strokes: List(SketchStroke)) -> String { |
| let smoothness = |
| list.fold(strokes, 0.0, fn(acc, stroke) { |
| acc +. calculate_curve_smoothness(stroke.points) |
| }) /. int.to_float(list.length(strokes)) |
|
|
| "Curve Analysis: " <> |
| float.to_string(smoothness) <> |
| "% smoothness" |
| } |
|
|
| fn calculate_curve_smoothness(points: List(#(Float, Float))) -> Float { |
| // Simplified - would use actual curvature analysis |
| case list.length(points) { |
| n if n > 10 -> 85.0 |
| n if n > 5 -> 70.0 |
| _ -> 50.0 |
| } |
| } |
|
|
| pub fn get_tracing_hints( |
| _reference_image: String, |
| current_progress: Float, |
| ) -> List(String) { |
| case current_progress { |
| p if p <. 30.0 -> [ |
| "💡 Start by outlining the basic shape", |
| "Focus on the main contours first", |
| ] |
| p if p <. 70.0 -> [ |
| "💡 Add the internal structures", |
| "Pay attention to proportions", |
| ] |
| _ -> [ |
| "💡 Refine details and shading", |
| "Your sketch looks great!", |
| ] |
| } |
| } |
|
|
|
|
|
|
|
|
|
|