diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..be8298c --- /dev/null +++ b/.ignore @@ -0,0 +1,3 @@ +*.ttf +*.woff2 +*.png diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 8459dc6..5030c58 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -401,6 +401,7 @@ unsafe extern "C" fn haku_compile_brush( "compiling: parsed successfully into {} AST nodes", ast.len() ); + // debug!("ast: {}", ast::dump::dump(&ast, root, Some(code))); let src = Source { code, ast: &ast }; @@ -431,6 +432,7 @@ unsafe extern "C" fn haku_compile_brush( ); debug!("compiling: {closure_spec:?}"); + /* debug!("bytecode: {:?}", chunk.bytecode); { let mut cursor = 0_usize; @@ -446,7 +448,7 @@ unsafe extern "C" fn haku_compile_brush( cursor += info.len as usize; } } - + // */ instance.compile_result2 = Some(CompileResult { defs_string: instance.defs.serialize_defs(), tags_string: instance.defs.serialize_tags(), diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index a458c5e..f3f8f3d 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -361,52 +361,43 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) - } let name = src.ast.span(op).slice(src.code); - if name == "=" { - c.emit(Diagnostic::error( - src.ast.span(op), - "defs `a = b` may only appear at the top level", - )); - return Ok(()); + match name { + ":" => { + // Invalid use of a def inside an expression. + c.emit(Diagnostic::error( + src.ast.span(op), + "defs `a: b` may only appear at the top level", + )); + Ok(()) + } + "." => compile_dot_call(c, src, left, right), + "|" => compile_pipe_call(c, src, left, right), + _ => { + compile_expr(c, src, left)?; + compile_expr(c, src, right)?; + if let Some(index) = system::resolve(SystemFnArity::Binary, name) { + let argument_count = 2; + c.chunk.emit_opcode(Opcode::System)?; + c.chunk.emit_u8(index)?; + c.chunk.emit_u8(argument_count)?; + } else { + c.emit(Diagnostic::error( + src.ast.span(op), + "this binary operator is currently unimplemented", + )); + } + Ok(()) + } } - - compile_expr(c, src, left)?; - compile_expr(c, src, right)?; - if let Some(index) = system::resolve(SystemFnArity::Binary, name) { - let argument_count = 2; - c.chunk.emit_opcode(Opcode::System)?; - c.chunk.emit_u8(index)?; - c.chunk.emit_u8(argument_count)?; - } else { - c.emit(Diagnostic::error( - src.ast.span(op), - "this unary operator is currently unimplemented", - )); - } - - Ok(()) } -fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult { - let mut walk = src.ast.walk(node_id); - let Some(func) = walk.node() else { - return Ok(()); - }; +fn emit_nary_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + func: NodeId, + argument_count: u8, +) -> CompileResult { let name = src.ast.span(func).slice(src.code); - - let mut argument_count = 0; - while let Some(arg) = walk.node() { - compile_expr(c, src, arg)?; - argument_count += 1; - } - - let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { - c.emit(Diagnostic::error( - src.ast.span(node_id), - "function call has too many arguments", - )); - 0 - }); - if let (NodeKind::Ident, Some(index)) = ( src.ast.kind(func), system::resolve(SystemFnArity::Nary, name), @@ -423,6 +414,81 @@ fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> c.chunk.emit_opcode(Opcode::Call)?; c.chunk.emit_u8(argument_count)?; } + Ok(()) +} + +fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult { + let mut walk = src.ast.walk(node_id); + let Some(func) = walk.node() else { + return Ok(()); + }; + + let mut argument_count = 0; + while let Some(arg) = walk.node() { + compile_expr(c, src, arg)?; + argument_count += 1; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.emit(Diagnostic::error( + src.ast.span(node_id), + "function call has too many arguments", + )); + 0 + }); + + emit_nary_call(c, src, func, argument_count)?; + + Ok(()) +} + +fn compile_dot_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + func: NodeId, + right: NodeId, +) -> CompileResult { + compile_expr(c, src, right)?; + emit_nary_call(c, src, func, 1)?; + + Ok(()) +} + +fn compile_pipe_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + left: NodeId, + call: NodeId, +) -> CompileResult { + match src.ast.kind(call) { + NodeKind::Call => { + let mut walk = src.ast.walk(call); + let Some(func) = walk.node() else { + return Ok(()); + }; + + compile_expr(c, src, left)?; + let mut argument_count = 1; + while let Some(arg) = walk.node() { + compile_expr(c, src, arg)?; + argument_count += 1; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.emit(Diagnostic::error( + src.ast.span(call), + "function call has too many arguments", + )); + 0 + }); + + emit_nary_call(c, src, func, argument_count)?; + } + _ => { + compile_expr(c, src, left)?; + emit_nary_call(c, src, call, 1)?; + } + } Ok(()) } diff --git a/crates/haku/src/lexer.rs b/crates/haku/src/lexer.rs index f3e4ce2..781d7d4 100644 --- a/crates/haku/src/lexer.rs +++ b/crates/haku/src/lexer.rs @@ -57,7 +57,11 @@ fn one_or_two(l: &mut Lexer<'_>, kind1: TokenKind, c2: char, kind2: TokenKind) - } fn is_ident_char(c: char) -> bool { - matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '\'' | '?') + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') +} + +fn is_ident_extra_char(c: char) -> bool { + matches!(c, '\'' | '?') } fn ident(l: &mut Lexer<'_>) -> TokenKind { @@ -65,6 +69,9 @@ fn ident(l: &mut Lexer<'_>) -> TokenKind { while is_ident_char(l.current()) { l.advance(); } + while is_ident_extra_char(l.current()) { + l.advance(); + } let end = l.position; match Span::new(start, end).slice(l.input) { @@ -208,6 +215,8 @@ fn token(l: &mut Lexer<'_>) -> (TokenKind, Span, bool) { '!' => one_or_two(l, TokenKind::Not, '=', TokenKind::NotEqual), '<' => one_or_two(l, TokenKind::Less, '=', TokenKind::LessEqual), '>' => one_or_two(l, TokenKind::Greater, '=', TokenKind::GreaterEqual), + '.' => one(l, TokenKind::Dot), + '|' => one(l, TokenKind::Pipe), '\n' => return newline(l, has_left_space), '(' => one(l, TokenKind::LParen), diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index a8e0cee..dd2884e 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -311,31 +311,13 @@ enum Tighter { fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - enum Spacing { - Loose, + enum Tightness { + Loose(usize), Call, - Tight, + Tight(usize), } - fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option<(Spacing, usize)> { - let spacing = match kind { - // There are a few types of operators which are independent of tightness. - - // For : and =, it does not matter if they're spelled one way or the other, because - // there is only one way to use them (at the beginning of the expression). - TokenKind::Colon | TokenKind::Equal => Spacing::Loose, - - // For calls, there is a special intermediate level, such that they can sit between - // loose operators and tight operators. - _ if PREFIX_TOKENS.contains(kind) => Spacing::Call, - - // For everything else, the usual rules apply. - _ => match spaces.pair() { - (false, false) => Spacing::Tight, - (true, true) => Spacing::Loose, - _ => return None, // not a valid infix operator - }, - }; + fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option { let index = match kind { TokenKind::Equal | TokenKind::Colon => 0, // 1: reserved for `and` and `or` @@ -345,12 +327,36 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual => 2, - TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::Slash => 3, - // 4: reserve for `.` - _ if PREFIX_TOKENS.contains(kind) => 5, + + TokenKind::Plus + | TokenKind::Minus + | TokenKind::Star + | TokenKind::Slash + | TokenKind::Pipe => 3, + + TokenKind::Dot => 4, + _ if is_prefix_token((kind, spaces)) => 5, _ => return None, // not an infix operator }; - Some((spacing, index)) + Some(match kind { + // There are a few types of operators which work independent of tightness. + TokenKind::Colon | TokenKind::Equal | TokenKind::Pipe => Tightness::Loose(index), + + // For unary -, we treat it as having Tight spacing rather than Call, else it would + // be allowed to begin function calls. + TokenKind::Minus if !spaces.are_balanced() => Tightness::Tight(index), + + // For calls, there is a special intermediate level, such that they can sit between + // loose operators and tight operators. + _ if is_prefix_token((kind, spaces)) => Tightness::Call, + + // For everything else, the usual rules apply. + _ => match spaces.pair() { + (false, false) => Tightness::Tight(index), + (true, true) => Tightness::Loose(index), + _ => return None, // not a valid infix operator + }, + }) } let Some(right_tightness) = tightness(right) else { @@ -361,6 +367,12 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { return Tighter::Right; }; + // When we're inside a call, subsequent arguments must not be slurped into the current + // expression, as it would result in calls being parsed as (vec (1 (-1))), which is not correct. + if left_tightness == Tightness::Call && right.0 == TokenKind::Minus && !right.1.are_balanced() { + return Tighter::Left; + } + if right_tightness > left_tightness { Tighter::Right } else { @@ -570,13 +582,15 @@ fn if_expr(p: &mut Parser) -> Closed { p.close(o, NodeKind::If) } +// TODO: There is a lot of special casing around `-` being both a prefix and an infix token. +// Maybe there's a way to simplify it? + // NOTE: This must be synchronised with the match expression in prefix(). const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ TokenKind::Ident, TokenKind::Tag, TokenKind::Number, TokenKind::Color, - TokenKind::Minus, TokenKind::Not, TokenKind::LParen, TokenKind::Backslash, @@ -584,6 +598,10 @@ const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ TokenKind::LBrack, ]); +fn is_prefix_token((kind, spaces): (TokenKind, Spaces)) -> bool { + PREFIX_TOKENS.contains(kind) || (kind == TokenKind::Minus && !spaces.are_balanced()) +} + fn prefix(p: &mut Parser) -> Closed { match p.peek() { TokenKind::Ident => one(p, NodeKind::Ident), @@ -592,16 +610,17 @@ fn prefix(p: &mut Parser) -> Closed { TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus | TokenKind::Not => unary(p), + TokenKind::Minus => unary(p), + TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), _ => { assert!( - !PREFIX_TOKENS.contains(p.peek()), - "{:?} found in PREFIX_TOKENS", - p.peek() + !is_prefix_token(p.peek_with_spaces()), + "{:?} is not a prefix token", + p.peek_with_spaces() ); let span = p.span(); @@ -615,9 +634,9 @@ fn prefix(p: &mut Parser) -> Closed { } fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { - match op.0 { + let (kind, spaces) = op; + match kind { TokenKind::Plus - | TokenKind::Minus | TokenKind::Star | TokenKind::Slash | TokenKind::EqualEqual @@ -626,11 +645,14 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual - | TokenKind::Colon => infix_binary(p, op), + | TokenKind::Colon + | TokenKind::Dot + | TokenKind::Pipe => infix_binary(p, op), + TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op), TokenKind::Equal => infix_let(p, op), - _ if PREFIX_TOKENS.contains(op.0) => infix_call(p, op), + _ if is_prefix_token(op) => infix_call(p, op), _ => panic!("unhandled infix operator {op:?}"), } @@ -650,7 +672,7 @@ fn infix_binary(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { } fn infix_call(p: &mut Parser, mut arg: (TokenKind, Spaces)) -> NodeKind { - while PREFIX_TOKENS.contains(p.peek()) { + while is_prefix_token(p.peek_with_spaces()) { precedence_parse(p, arg); arg = p.peek_with_spaces(); } diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index c613408..a005630 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -40,6 +40,8 @@ pub enum TokenKind { Colon, Backslash, RArrow, + Dot, + Pipe, // Keywords Underscore, @@ -135,6 +137,10 @@ impl Spaces { pub fn pair(self) -> (bool, bool) { (self.left(), self.right()) } + + pub fn are_balanced(self) -> bool { + matches!(self.pair(), (true, true) | (false, false)) + } } impl fmt::Debug for Spaces { diff --git a/crates/haku2/build.rs b/crates/haku2/build.rs index a68dc93..3d49a8b 100644 --- a/crates/haku2/build.rs +++ b/crates/haku2/build.rs @@ -46,6 +46,7 @@ fn main() -> Result<(), Box> { .arg("--prominent-compile-errors") .arg("--color") .arg(color) + .arg("-freference-trace") // Build output .arg("--cache-dir") .arg(out_path.join("zig-cache")) diff --git a/crates/haku2/src/haku2.zig b/crates/haku2/src/haku2.zig index ec428fc..136f21f 100644 --- a/crates/haku2/src/haku2.zig +++ b/crates/haku2/src/haku2.zig @@ -22,6 +22,7 @@ pub const std_options: std.Options = .{ }; pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool { + // Replace any of the false returns below to enable log scopes for the build. if (scope == .vm) return false else diff --git a/crates/haku2/src/value.zig b/crates/haku2/src/value.zig index d1498ff..3269a60 100644 --- a/crates/haku2/src/value.zig +++ b/crates/haku2/src/value.zig @@ -48,7 +48,7 @@ pub const Value = union(enum) { } pub fn gt(a: Value, b: Value) ?bool { - return !a.eql(b) and !(a.lt(b) orelse return null); + return b.lt(a); } pub fn typeName(value: Value) []const u8 { @@ -69,24 +69,24 @@ pub const Value = union(enum) { }; } - pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(value: Value, writer: *std.Io.Writer) !void { switch (value) { - .nil => try std.fmt.formatBuf("Nil", options, writer), - .false => try std.fmt.formatBuf("False", options, writer), - .true => try std.fmt.formatBuf("True", options, writer), - inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}), - inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }), + .nil => try writer.writeAll("Nil"), + .false => try writer.writeAll("False"), + .true => try writer.writeAll("True"), + inline .tag, .number => |x| try writer.print("{d}", .{x}), + inline .vec4, .rgba => |x| try writer.print("{s}{d}", .{ @tagName(value), x }), .ref => |ref| switch (ref.*) { - .closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}), + .closure => |c| try writer.print("function {s}", .{c.name}), .list => |l| { - try std.fmt.formatBuf("[", options, writer); + try writer.writeAll("["); for (l, 0..) |elem, i| { - if (i != 0) try std.fmt.formatBuf(", ", options, writer); - try elem.format(fmt, options, writer); + if (i != 0) try writer.writeAll(", "); + try elem.format(writer); } - try std.fmt.formatBuf("]", options, writer); + try writer.writeAll("]"); }, - inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}), + inline .shape, .scribble, .reticle => |x| try writer.print("{}", .{x}), }, } } diff --git a/crates/haku2/src/vm.zig b/crates/haku2/src/vm.zig index 9f90953..7bec9a1 100644 --- a/crates/haku2/src/vm.zig +++ b/crates/haku2/src/vm.zig @@ -147,6 +147,24 @@ pub fn stackFrame(vm: *const Vm, index: usize) StackFrame { } } +const StackFmt = struct { + stack: []const Value, + + pub fn format(f: *const StackFmt, writer: *std.Io.Writer) !void { + try writer.writeAll("["); + for (f.stack, 0..) |val, i| { + if (i != 0) + try writer.writeAll(", "); + try val.format(writer); + } + try writer.writeAll("]"); + } +}; + +fn stackFmt(stack: []const Value) StackFmt { + return .{ .stack = stack }; +} + /// Debug assertion for bytecode validity. /// In future versions, this may become disabled in release builds. fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void { @@ -167,7 +185,7 @@ pub fn push(vm: *Vm, val: Value) Error!void { if (vm.stack_top >= vm.stack.len) { return vm.throw("too many live temporary values (local variables and expression operands)", .{}); } - log.debug("PUSH {any} <- {}", .{ vm.stack[0..vm.stack_top], val }); + log.debug("PUSH {f} <- {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), val }); vm.stack[vm.stack_top] = val; vm.stack_top += 1; } @@ -176,7 +194,7 @@ pub fn pop(vm: *Vm) Error!Value { try vm.validateBytecode(vm.stack_top > 0, "value stack underflow", .{}); vm.stack_top -= 1; const result = vm.stack[vm.stack_top]; - log.debug("POP {any} -> {}", .{ vm.stack[0..vm.stack_top], result }); + log.debug("POP {f} -> {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), result }); return vm.stack[vm.stack_top]; } diff --git a/docs/haku.dj b/docs/haku.dj new file mode 100644 index 0000000..aa93e43 --- /dev/null +++ b/docs/haku.dj @@ -0,0 +1,520 @@ +# haku reference manual + +haku is a dynamically typed, pure functional programming language, used for programming brushes in rakugaki. + +For the layperson, it can be thought of a beefed up calculator. +It has roots in ordinary school algebra, but has a much more powerful set of features, with the special ability of being able to edit images. + + +## Overview + +haku programs are called _brushes_. +The purpose of a brush is to produce instructions for rakugaki on how the user may interact with the wall (via _reticles_), and what things should be drawn on the wall as a result of those interactions (via _scribbles_). +Collectively, reticles and scribbles are what we call _effects_. + +A brush's task is to compute (or in simpler terms, calculate) an effect that rakugaki will then _perform_. + +In case of reticles, rakugaki will allow the user to interact with the wall, and then ask haku for another effect to perform afterwards (a _continuation_). + +In case of scribbles, rakugaki will draw the scribble onto the wall, without asking haku for more. + +Once rakugaki runs through all effects, we say that the brush has _finished executing_. + + +## Lexical elements + + +### Comments + +haku's most basic lexical element is the _comment_. + +``` +-- This is a comment. +-- こんにちは! +``` + +Comments introduce human-readable remarks into a brush's code. +Their only purpose is documentation. +They serve no semantic meaning, and thus do not affect the result of the brush whatsoever. + +Comments begin with `--`, and span until the end of the current line of text. +Once the line ends, the comment does, too. + +Comments do not necessarily have to appear at the beginning of a line. + +```haku +magnitude: \v -> -- calculate the magnitude of a vector + hypot vecX.v vecY.v +``` + + +### Literals + +Literals represent _literal values_ that are input into the brush. + +haku has a few types of literals, but not all literals are purely lexical elements (some of them can nest)---which is why the different types of literals are covered under the [Expressions](#Expressions) section. + + +### Identifiers + +Identifiers are used for naming values inside a brush. +New names are introduced using [defs](#Structure-of-a-brush) and [lets](#Let-expression). +Once a name is introduced, it may be referenced using its identifier in its corresponding scope---the whole brush for defs, and the following expression in lets. + +An identifier starts with a *lowercase* ASCII letter---`a`--`z` or an underscore---`_`, and is followed by zero or more ASCII letters of any case---`a`--`z`, `A`--`Z`, digits---`0`--`9`, and underscores---`_`. +This then may be followed by an arbitrary number of _suffix characters_ prime symbols---`'` and question marks---`?`. + +By convention, prime symbols are used in the same way they are used in math notation---for introducing a distinct variable of the same name as another variable, usually derived from the previous. +For example, given a variable named `position`, an _updated_ position may be named `position'`. + +The question mark suffix is conventionally used for [boolean](#Boolean-type) variables, as well as boolean-returning functions. +By convention, only one question mark is always used. + +Identifiers starting with *uppercase* ASCII letters---`A`--`Z` are not identifiers, but rather [tags](#tag-literal), and therefore cannot be used as def and let names. + +The following identifiers are reserved as _keywords_, and have special meaning assigned within the language syntax. + +- `if` --- Introduces an [`if` expression](#if-expression). +- `else` --- Introduces the `else` clause in an [`if` expression](#if-expression). + +Additionally, the following identifiers are reserved for future use, and may not be used for naming defs and lets. + +- `and` +- `or` + +By convention, a prime symbol `'` suffix can be used to work around this restriction. +For example, instead of naming a variable `if`, try naming it `if'` (read as _if prime_, "_the other if_"). + + +### Operators and punctuation + +Operators and punctuation share a section in this part of the reference due to their lexical similarity. + +[Operators](#operators) serve as a terse syntax for calling a small set of built-in functions within the program (described in detail in the [system reference documentation](system.html)), while punctuation serves the purpose of syntactic delimiters. + +The following symbols are operators: + +``` ++ - * / +== != < <= > >= ! +``` + +And the following symbols are punctuation: + +``` + +( ) [ ] , += : +. | +\ -> +``` + +`` is literally written down as a line break in programs, which would be invisible in this reference. + + +## Structure of a brush + +A brush is structured like so: + +```haku +def1: expr1 +def2: expr2 +def3: expr3 +-- ... +effectExpr +``` + +That is, there are two parts to a brush: its _defs_, followed by the resulting effect [expression](#Expressions). +The effect produced by this expression is the effect performed by rakugaki when the user interacts with the wall (clicks on it, touches it, starts a pen stroke). + +Defs introduce names for values that are available across the entire program. +They are most commonly used to name constants and functions. + +Example: + +```haku +-- Constant definition +pi: 3.14159265 + +-- Function definition +magnitude: \v -> + hypot vecX.v vecY.v +``` + + +## Expressions + +haku is a strictly expression-oriented language. +There is no idea of statements, as would be the case in lower-level languages like C++ or JavaScript. +This comes from the fact that haku is a _pure_ functional language, which means there aren't any expressions whose result you would want to discard, only for their side effects. + + +### Nil + +An empty parenthesis represents a _nil_ value---that is, literally a value that means "no value." + +```haku +() +``` + +It is one of the only values considered [_false_](#Truth) by the language, other than the [`False` boolean](#Tags). + + +### Numbers + +Numbers in haku are written down as a sequence of ASCII digits---`0`--`9`, followed by an optional decimal point `.` with a decimal part. + +```haku +0 +123 +3.14159265 +``` + +Internally, they are represented by 32-bit floating point numbers. +This means that they have [fairly limited precision](https://float.exposed/0x42f60000), and do not always behave exactly like math on real numbers. +For example, there are magical values for ∞ and -∞ (which _can_ exist and can be operated upon), as well as a value called NaN (not a number), which are produced as results of certain operations that aren't well-defined in math (most commonly division by zero). + + +### Tags + +Tags are similar to [identifiers](#Identifiers), but start with an *uppercase* rather than a lowercase ASCII letter (that is, `A`--`Z`). +They are values which represent _names_. + +This concept may feel a bit alien. +As an example, consider how haku implements record types. +From the perspective of the user, a record type acts like a [function](#Functions) which accepts a tag as an argument---with the tag being the name of the record field. + +```haku +withDotter \d -> + stroke 8 #000 (line d.From d.To) + ---- -- these are tags +``` + +There are also two special tags, `True` and `False`, which are used to represent Boolean logic. + +The boolean `False` is the only [_false_](#Truth) value in the language, other than [nil](#Nil). + + +### Colors + +haku has a literal for representing RGBA colors. +It takes one of the following four forms, from most explicit to least explicit: + +```haku +#RRGGBBAA +#RRGGBB +#RGBA +#RGB +``` + +Each character in a color literal is a hexadecimal digit, with the digits `0`--`9` representing digits 0 to 9, and letters `a`--`f` representing digits 10 to 16 (case insensitive.) + +For `#RGB` and `#RGBA`, the digits are repeated twice implicitly---that is, `#1234` is the same as `#11223344`. + +This syntax is designed to be convenient for working with colors coming from external programs. +For example, you may pick a color from an online palette, and paste its hex code straight into your brush code. + +Example (rakugaki logo colors): + +```haku +white: #FFF +peach: #FFA6A6 +raspberry: #F44096 +``` + + +### Lists + +Lists are fixed-length sequences of values. +They are written down by listing out a sequence of comma `,` or newline-separated items, enclosed in square brackets `[]`. + +```haku +six: [1, 2, 3, 4, 5, 6] +four: [ + 1 + 2 + 3 + 4 +] +``` + +Lists are allowed to nest, as the values may be of any type---including lists themselves. + +Lists are most commonly used to compose scribbles. +A list is also a scribble, which draws the scribbles it contains within itself, from first to last. + + +### Operators + +Operators in haku are used mostly for basic mathematical operations. +They are either unary, written in prefix form `!x`, or binary, written in infix form `a + b`. + +While order of operations in case of unary operators is unambiguous (innermost to outermost), infix operators are not as simple. +Certain infix operators have _precedence_ over others. + +This precedence depends on the operator used, as well as the spacing around it. +Spacing _only_ matters for infix operators; prefix operators may have any amount of spaces around them, though conventionally they are glued to the expression on the right, like `(-1)`, or `vec -1 0`. + +Infix operators with spaces around them are classified as _loose_, and those without spaces around them are _tight_. +An unequal amount of spaces around an infix operator is considered an error (or is parsed as a prefix operator, depending on context). + +Based on these two groups, the precedence of operators is defined as follows: + +1. Prefix operators: `-a`, `!a` +1. Tight + + 1. Dot: `a.b` + 1. Arithmetic: `a+b`, `a-b`, `a*b`, `a/b` + 1. Comparisons: `a==b`, `a!=b`, `ab`, `a>=b` + +1. Function calls: `a b` +1. Loose + + 1. Dot: `a . b` + 1. Arithmetic: `a + b`, `a - b`, `a * b`, `a / b`, `a |b` + 1. Comparisons: `a == b`, `a != b` + 1. Variables: `a: b`, `a = b` + +The operators `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `!`, are used for calling functions built into the haku [system library](system.html). + +Other infix tokens listed above have other semantic meaning. + +- `a b`, `.`, and `|` --- Used for calling [functions](#Functions). +- `:` --- Used for introducing [defs](#Structure-of-a-brush). +- `=` --- Used for introducing [lets](#Let-expression). + +Examples of how these precedence rules work in practice: + +```haku +2 + 2 * 2 == 8 -- left to right +2 + 2*2 == 6 -- 2*2 goes first +2+2 * 2 == 8 -- 2+2 goes first +2+2*2 == 8 -- left to right + +sin 2 * pi * x == (sin 2) * pi * x -- function call goes first +sin 2*pi*x == sin (2 * pi * x) -- argument goes first + +-- unintuitive for users of C-likes: +-- prefix `-` has higher precedence than `.` +-vecX.v == (-vecX).v +``` + +One thing that should be noted about haku's operator precedence is that, unlike math notation and most other programming languages, `+`, `-`, `*`, and `/`, are evaluated from left to right. +This is because otherwise they would interact unintuitively with the pipe `|` operator, which is effectively used as an operator that turns any function infix. + +It is not obvious where `|` would sit in the precedence hierarchy if arithmetic was split into separate precedence levels for `+` and `-`, and `*` and `/`, whereas with haku's solution, all arithmetic expressions are simply read from left to right. + + +### Parentheses + +In case the tight-loose system is not expressive enough, parentheses can be used as an escape hatch for grouping expressions. + +```haku +2 + (2 * 2) == 2 + 2*2 +``` + + +### Functions + +Functions in haku follow the definition of mathematical functions: given a set of _arguments_, the arguments are substituted into the function's _parameter variables_, and a result is computed from the resulting expression. + +A function literal is written down like `\params -> result`, where `params` is a comma-separated list of parameters, and `result` is the function's resulting expression. + +```haku +square: \x -> x * x +``` + +A newline is permitted after the arrow `->`, which should be preferred for most functions. + +```haku +magnitude: \v -> + hypot vecX.v vecY.v + +normalize: \v -> + l = magnitude v + v / vec l l +``` + +Note that there must be at least one parameter. +In case you need a function with no parameters, you almost always want a constant value instead. + +Functions can be used by _calling_ them with space-separated arguments. +Note that space-separated function calls have higher precedence than most arithmetic operators, which means you have to use parentheses or tighten the expression to pass more complicated expressions. + +```haku +normalize (vec 4 4) +``` + +Note that a call must pass in _exactly_ the amount of arguments defined by the function. +Calling the above-defined `normalize` with more than one argument will not work: + +```haku +normalize (vec 4 4) (vec 5 5) +``` + +In places where a call with space-separated arguments is inconvenient, there are two alternative syntaxes for calling a function. + +The first is by using the `.` operator, which, when used tightly, can be used to do a function call with higher operator precedence than an ordinary space-separated call would have. + +```haku +f x == f.x +``` + +Combined with [records](#Records) and [tags](#Tags), it mimics the record field access syntax found in C-like programming languages. + +```haku +withDotter \d -> + stroke 8 #000 (line d.From d.To) + ------ ---- +``` + +The other alternative syntax is the _pipe_ `|` operator. +It calls the function on the right with the argument on the left. + +```haku +2 |sqrt +``` + +When a space-separated function call is found on the right, the `|` operator instead inserts the value from the left as the first argument of the function call. + +```haku +x |mod 2 == mod x 2 +``` + +The spacing convention around `|` is a bit unusual, but the above example demonstrates why: the `|` operator effectively turns an arbitrary function into an infix operator on par with built-in arithmetic operators. +Therefore, the function name is glued to the pipe, like `|mod`, to appear as one word visually. + +Certain functions are _built-in_, and implement core functionality that cannot be implemented in haku alone (at all, or in a performant manner). +The [haku system library](system.html) is what defines all the built-in functions. + +Due to temporary limitations of the implementation, built-in functions cannot be referenced like regular functions. +They always have to appear in a call. + +If you'd like to reference a built-in function to e.g. pass it to a list-transforming function, you will have to wrap it in a function of your own: + +```haku +add: \x, y -> x + y +sum: [1, 2, 3] |reduce 0 sum + +sin': \x -> sin x +sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin' +``` + + +### `if` expressions + +`if` expressions allow for choosing between two different results based on a condition. + +```haku +if (cond) + a +else + b +``` + +When `cond` is [_true_](#Truth), `a` will be computed and returned as the result. +Otherwise, `b` will be computed and returned as the result. + +Note that in both cases, only one of the two expressions is computed. +This allows for implementing bounded recursion to achieve repetition. + +```haku +-- Fibonacci sequence +fib: \x -> + if (x > 1) + fib n-1 + fib n-2 + else + x +``` + + +### Let expressions + +Let expressions introduce a new _variable_, or _let_, that can be referenced in the expression on the next line. + +```haku +x = y +expr +``` + +The difference between lets and [defs](#Structure-of-a-brush) is that the value of a let can change, because it can depend on non-def values, such as function arguments (therefore making it _variable_.) + +This however means that lets have reduced _scope_. +The name introduced by a def can be used in the entire program---even before the line it's introduced on---while the name introduced by a let can only be used in the expression that immediately follows the let. + +```haku +x: 1 + +f: \y -> + z = x + 1 + x + y + z -- ok + +z -- not ok +``` + +This also means that lets cannot be used to create recursive functions, because the name introduced by a let only becomes visible in its following expression. + +```haku +-- This is okay: +defib: \x -> + if (x > 1) + defib n-1 + defib n-2 + else + x + +-- This will not work: +letfib = \x -> + if (x > 1) + -- Because letfib is not yet defined at this point. + letfib n-1 + letfib n-2 + else + x + +defib 5 + letfib 5 -- It is only defined in this expression. +``` + +Since a let can be followed up by any other expression, multiple lets can be chained together. + +``` +x = 4 +y = x + 3 +x + y +``` + +Note however that `x + y` finishes the chain of lets, which means we cannot introduce additional ones after that line. +That would begin another expression! + + +## Types + +haku distinguishes values between a few different types. + +- [*nil*](#Nil) +- [*tag*](#Tags) + + - [*boolean*]{id="Boolean-type"} - either [`False` or `True`](#Tags). Indicates truth or falsehood, used in [`if` conditions](#if-expression). + +- [*number*](#Numbers) +- *vec* - a 4-dimensional vector, composed of four `number`s. +- [*rgba*](#Colors) - an RGBA color, composed of four `number`s. This is the type of color literals. +- [*function*](#Functions) +- [*list*](#Lists) +- *shape* - a mathematical shape. +- _*effect*_ - an action that can be performed by rakugaki. + + - *scribble* - something that can be drawn on the wall. + - *reticle* - an interaction the user can make with the wall. + +These types are incompatible with each other. +If you pass in a *tag* to a value that expects a *number*, you will get an error. + +You can refer to the [system library documentation](system.html) for more information on the types accepted by functions. +Note that it has a more precise notation for describing types, which explicitly documents the types of values that will be found in more nuanced situations, such as the `map` function. + + +### Truth + +Conditions in `if` expressions, as well as the `!` operator, consider certain types of values _truthy_, and others _falsy_. + +Falsy values include *nil* and the boolean `False`. +All other values are considered truthy. diff --git a/docs/rkgk.dj b/docs/rkgk.dj index 7335554..6c89a89 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -1,5 +1,15 @@ # Introduction to rakugaki +*Warning: Neither rakugaki, nor this introductory manual is finished.* +While it will always use the most up-to-date and recommended syntax, there are things it does cover, and it will probably be rebuilt to improve coverage of features once the app stabilises a bit. + +For now, I recommend cross-referencing it with the following documents: + +- [haku language reference](haku.html) +- [System library](system.html) + +--- + Welcome to rakugaki! I hope you've been having fun fiddling with the app so far. @@ -28,7 +38,7 @@ In case you edited anything in the input box on the right, paste the following t -- and see what happens! withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` rakugaki is a drawing program for digital scribbles and other pieces of art. @@ -86,8 +96,8 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we -- Draw two colorful dots instead of one! withDotter \d -> [ - stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #F00 (d.To + vec 4 0) + stroke 8 #00F (d.To + vec -4 0) ] ``` @@ -108,26 +118,16 @@ It'll draw the first inner list, which contains two scribbles, and then it'll dr withDotter \d -> [ [ - stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #F00 (d.To + vec 4 0) + stroke 8 #00F (d.To + vec -4 0) ] [ - stroke 8 #FF0 (d To + vec 0 4) - stroke 8 #0FF (d To + vec 0 (-4)) + stroke 8 #FF0 (d.To + vec 0 4) + stroke 8 #0FF (d.To + vec 0 -4) ] ] ``` -::: aside - -Another weird thing: when negating a number, you have to put it in parentheses. - -This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing! -In this case, it will always choose the 2nd interpretation---vec minus four. -So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses. - -::: - This might seem useless, but it's a really useful property in computer programs. It essentially means you can snap pieces together like Lego bricks! @@ -146,7 +146,7 @@ Recall that super simple brush from before... ```haku withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` This reads as "given a dotter, output a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the dotter's `To` coordinates." @@ -179,6 +179,7 @@ If you reorder or remove any one of them, your brush isn't going to work! You can also specify an alpha channel, for transparent colors---`#RRGGBBAA`, or `#RGBA`. - The third ingredient is the stroke's _position_. + `d.To` is the position of your mouse cursor. Positions in haku are represented using mathematical _vectors_, which, when broken down into pieces, are just lists of some numbers. @@ -186,7 +187,7 @@ haku vectors however are a little more constrained, because they always contain We call these four numbers X, Y, Z, and W respectively. Four is a useful number of dimensions to have, because it lets us do 3D math---which technically isn't built into haku, but if you want it, it's there. -For most practical purposes, we'll only be using the first _two_ of the four dimensions though---X and Y. +For most practical purposes, we'll only be using the first _two_ of the four dimensions though---X and Y. This is because the wall is a 2D space---it's a flat surface with no depth. It's important to know though that vectors don't mean much _by themselves_---rakugaki just chooses them to represent points on the wall, but in a flat 2D space, all points need to be relative to some _origin_---the vector `(0, 0)`. @@ -197,22 +198,22 @@ Likewise, negative X coordinates go leftwards, and negative Y coordinates go upw --- -Vectors in haku are obtained with another function---`vec`---though we don't use it in the basic example, because `d To` already is a vector. -Vectors support all the usual math operators though, so if we wanted to, we could, for example, add a vector to `d To`, thus moving the position of the dot relative to the mouse cursor: +Vectors in haku are obtained with another function---`vec`---though we don't use it in the basic example, because `d.To` already is a vector. +Vectors support all the usual math operators though, so if we wanted to, we could, for example, add a vector to `d.To`, thus moving the position of the dot relative to the mouse cursor: ```haku withDotter \d -> - stroke 8 #000 (d To + vec 10 0) -- moved 10 pixels rightwards + stroke 8 #000 (d.To + vec 10 0) -- moved 10 pixels rightwards ``` -Also note how the `d To` expression is parenthesized. +Also note how the `d.To + vec 10 0` expression is parenthesized. This is because otherwise, its individual parts would be interpreted as separate arguments to `stroke`, which is not what we want! Anyways, with all that, we let haku mix all the ingredients together, and get a black dot under the cursor. ```haku withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` Nice! @@ -231,10 +232,10 @@ Let's fix that by drawing a `line` instead! ```haku withDotter \d -> - stroke 8 #000 (line (d From) (d To)) + stroke 8 #000 (line d.From d.To) ``` -We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. +We replace the singular position `d.To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line. ::: aside @@ -249,16 +250,74 @@ haku also supports other kinds of shapes: circles and rectangles. ```haku withDotter \d -> [ - stroke 8 #F00 (circle (d To + vec (-16) 0) 16) - stroke 8 #00F (rect (d To + vec 0 (-16)) 32 32) + stroke 8 #F00 (circle (d.To + vec -16 0) 16) + stroke 8 #00F (rect (d.To + vec 0 -16) (vec 32 32)) ] ``` - `circle`s are made up of an X position, Y position, and radius. - + - `rect`s are made up of the (X and Y) position of their top-left corner, and a size (width and height).\ Our example produces a square, because the rectangle's width and height are equal! + +## Math in haku + +While haku is based entirely in pure math, it is important to note that haku is _not_ math notation! +It is a textual programming language, and has different rules concerning order of operations than math. + +::: aside + +If you've programmed in any other language, you might find those rules alien. +But I promise you, they make sense in the context of the rest of the language! + +::: + +In traditional math notation, the conventional order of operations is: + +1. Parentheses +2. Exponentiation +3. Multiplication and division +4. Addition and subtraction + +haku does not have an exponentiation operator. +That purpose is served by the function `pow`. +It does however have parentheses, multiplication, division, addition, and subtraction. + +Unlike in math notation, addition, subtraction, multiplication, and division, are _all_ calculated from left to right---multiplication and division does not take precedence over addition and subtraction. +So for the expression `2 + 2 * 2`, the result is `8`, and not `6`! + +Since this can be inconvenient at times, there is a way to work around that. +haku has a distinction between _tight_ and _loose_ operators, where tight operators always take precedence over loose ones in the order of operations. + +Remove the spaces around the `*` multiplication operator, like `2 + 2*2`, and the result is now `6` again---because we made `*` tight! + +This is convenient when representing fractions. +If you want a constant like half-π, the way to write it is `1/2*pi`---and order of operations will never mess you up, as long as you keep it tight without spaces! + +The same thing happens with functions. +For example, if you wanted to calculate the sine of `1/2*pi*x`, as long as you write that as `sin 1/2*pi*x`, with the whole argument without spaces, you won't have to wrap it in parentheses. + +Inside a single whole tight or loose expression, there is still an order of operations. +In fact, here's the full order of operations in haku for reference: + +1. Tight + + 1. Function applications: `.` + 1. Arithmetic: `+`, `-`, `*`, `/` + 1. Comparisons: `==`, `!=`, `<`, `<=`, `>`, `>=` + +1. Function calls +1. Loose + + 1. Function applications: `.` + 1. Arithmetic and pipelines `|` + 1. Comparisons + 1. Variables: `:`, `=` + +Naturally, you can still use parentheses when the loose-tight distinction is not enough. + + ## Programming in haku So far we've been using haku solely to describe data. @@ -270,7 +329,7 @@ Remember that example from before? withDotter \d -> [ stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #00F (d To + vec -4 0) ] ``` @@ -281,7 +340,7 @@ If we wanted to change the size of the points, we'd need to first update the str withDotter \d -> [ stroke 4 #F00 (d To + vec 4 0) - stroke 4 #00F (d To + vec (-4) 0) + stroke 4 #00F (d To + vec -4 0) --- ] ``` @@ -294,8 +353,8 @@ So we also have to update their positions. [ stroke 4 #F00 (d To + vec 2 0) --- - stroke 4 #00F (d To + vec (-2) 0) - -- + stroke 4 #00F (d To + vec -2 0) + -- ] ``` @@ -322,7 +381,7 @@ thickness: 4 withDotter \d -> [ stroke thickness #F00 (d To + vec 2 0) - stroke thickness #00F (d To + vec (-2) 0) + stroke thickness #00F (d To + vec -2 0) --------- ] ``` @@ -355,7 +414,7 @@ xOffset: 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) --------- ] ``` @@ -371,7 +430,7 @@ Uppercase names are special values we call _tags_. Tags are values which represent names. For example, the `To` in `d To` is a tag. -It represents the name of the piece of data we're extracting from `d`. +It is the name of the piece of data we're extracting from `d`. There are also two special tags, `True` and `False`, which represent [Boolean](https://en.wikipedia.org/wiki/Boolean_algebra) truth and falsehood. @@ -388,7 +447,7 @@ xOffset: 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) ] ``` @@ -402,7 +461,7 @@ xOffset: thickness / 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) ] ``` @@ -564,6 +623,7 @@ Seriously, 64 is my limit. I wonder if there's any way we could automate this? + ### The Ouroboros You know the drill by now. @@ -587,7 +647,7 @@ splat: \d, radius -> airbrush: \d, size -> [ splat d size - airbrush d (size - 8) + airbrush d size-8 ] withDotter \d -> @@ -649,7 +709,7 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 8) + airbrush d size-8 ] else [] @@ -675,8 +735,8 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 1) - --- + airbrush d size-1 + --- ] else [] @@ -696,7 +756,7 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 1) + airbrush d size-1 ] else [] diff --git a/rkgk.toml b/rkgk.toml index 5cd488c..21bc332 100644 --- a/rkgk.toml +++ b/rkgk.toml @@ -23,6 +23,7 @@ import_roots = [ "docs/rkgk.dj" = "Introduction to rakugaki" "docs/system.dj" = "System library" +"docs/haku.dj" = "haku language reference" [wall_broker.default_wall_settings] diff --git a/static/base.css b/static/base.css index 6fc9acd..bf170d6 100644 --- a/static/base.css +++ b/static/base.css @@ -53,6 +53,7 @@ code, textarea { font-family: var(--font-monospace); line-height: var(--line-height); + font-variant-ligatures: none; } /* Buttons */ diff --git a/static/brush-box.js b/static/brush-box.js index dfcc850..bee6be6 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -15,7 +15,7 @@ color: #000 thickness: 8 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -27,7 +27,7 @@ color: #000 thickness: 48 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -40,14 +40,10 @@ thickness: 4 length: 5 duty: 0.5 -or_: \\a, b -> - if (a) a - else b - withDotter \\d -> - visible? = mod (d Num) length < length * duty + visible? = d.Num |mod length < length * duty if (visible?) - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) else () `.trim(), @@ -61,7 +57,7 @@ color: #0003 thickness: 6 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -76,10 +72,10 @@ wavelength: 1 withDotter \\d -> pi = 3.14159265 - a = sin (d Num * wavelength / pi) + 1 / 2 + a = sin (d.Num * wavelength / pi) + 1 / 2 range = maxThickness - minThickness thickness = a * range + minThickness - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -93,22 +89,21 @@ amplitude: 50 wavelength: 1 mag: \\v -> - hypot (vecX v) (vecY v) + hypot vecX.v vecY.v norm: \\u -> l = mag u u / vec l l perpClockwise: \\v -> - vec (vecY v) (-(vecX v)) + vec vecY.v -(vecX.v) withDotter \\d -> pi = 3.14159265 - a = sin (d Num * wavelength / pi) * amplitude - direction = (d To) - (d From) - clockwise = norm (perpClockwise direction) * vec a a - from = d From + clockwise - to = d To + clockwise + a = sin (d.Num * wavelength / pi) * amplitude + clockwise = norm (perpClockwise d.To-d.From) * vec a a + from = d.From + clockwise + to = d.To + clockwise stroke thickness color (line from to) `.trim(), }, @@ -121,16 +116,16 @@ wavelength: 0.1 thickness: 8 colorCurve: \\n -> - abs (cos n) + n |cos |abs withDotter \\d -> pi = 3.14159265 l = wavelength - r = colorCurve (d Num * l) - g = colorCurve (d Num * l + pi/3) - b = colorCurve (d Num * l + 2*pi/3) + r = colorCurve (d.Num * l) + g = colorCurve (d.Num * l + pi/3) + b = colorCurve (d.Num * l + 2*pi/3) color = rgba r g b 1 - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), },