diff --git a/.ignore b/.ignore deleted file mode 100644 index be8298c..0000000 --- a/.ignore +++ /dev/null @@ -1,3 +0,0 @@ -*.ttf -*.woff2 -*.png diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 5030c58..8459dc6 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -401,7 +401,6 @@ 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 }; @@ -432,7 +431,6 @@ unsafe extern "C" fn haku_compile_brush( ); debug!("compiling: {closure_spec:?}"); - /* debug!("bytecode: {:?}", chunk.bytecode); { let mut cursor = 0_usize; @@ -448,7 +446,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 f3f8f3d..a458c5e 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -361,59 +361,28 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) - } let name = src.ast.span(op).slice(src.code); - 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(()) - } + if name == "=" { + c.emit(Diagnostic::error( + src.ast.span(op), + "defs `a = b` may only appear at the top level", + )); + 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); - if let (NodeKind::Ident, Some(index)) = ( - src.ast.kind(func), - system::resolve(SystemFnArity::Nary, name), - ) { + 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 { - // This is a bit of an oddity: we only emit the function expression _after_ the arguments, - // but since the language is effectless this doesn't matter in practice. - // It makes for a bit less code in the VM, since there's no need to find the function - // down the stack - it's always on top. - compile_expr(c, src, func)?; - c.chunk.emit_opcode(Opcode::Call)?; - c.chunk.emit_u8(argument_count)?; + c.emit(Diagnostic::error( + src.ast.span(op), + "this unary operator is currently unimplemented", + )); } + Ok(()) } @@ -422,6 +391,7 @@ fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> let Some(func) = walk.node() else { return Ok(()); }; + let name = src.ast.span(func).slice(src.code); let mut argument_count = 0; while let Some(arg) = walk.node() { @@ -437,57 +407,21 @@ fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> 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)?; - } + if let (NodeKind::Ident, Some(index)) = ( + src.ast.kind(func), + system::resolve(SystemFnArity::Nary, name), + ) { + c.chunk.emit_opcode(Opcode::System)?; + c.chunk.emit_u8(index)?; + c.chunk.emit_u8(argument_count)?; + } else { + // This is a bit of an oddity: we only emit the function expression _after_ the arguments, + // but since the language is effectless this doesn't matter in practice. + // It makes for a bit less code in the VM, since there's no need to find the function + // down the stack - it's always on top. + compile_expr(c, src, func)?; + c.chunk.emit_opcode(Opcode::Call)?; + c.chunk.emit_u8(argument_count)?; } Ok(()) diff --git a/crates/haku/src/lexer.rs b/crates/haku/src/lexer.rs index 781d7d4..f3e4ce2 100644 --- a/crates/haku/src/lexer.rs +++ b/crates/haku/src/lexer.rs @@ -57,11 +57,7 @@ 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' | '_') -} - -fn is_ident_extra_char(c: char) -> bool { - matches!(c, '\'' | '?') + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '\'' | '?') } fn ident(l: &mut Lexer<'_>) -> TokenKind { @@ -69,9 +65,6 @@ 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) { @@ -215,8 +208,6 @@ 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 dd2884e..a8e0cee 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -311,13 +311,31 @@ enum Tighter { fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - enum Tightness { - Loose(usize), + enum Spacing { + Loose, Call, - Tight(usize), + Tight, } - fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option { + 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 + }, + }; let index = match kind { TokenKind::Equal | TokenKind::Colon => 0, // 1: reserved for `and` and `or` @@ -327,36 +345,12 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual => 2, - - TokenKind::Plus - | TokenKind::Minus - | TokenKind::Star - | TokenKind::Slash - | TokenKind::Pipe => 3, - - TokenKind::Dot => 4, - _ if is_prefix_token((kind, spaces)) => 5, + TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::Slash => 3, + // 4: reserve for `.` + _ if PREFIX_TOKENS.contains(kind) => 5, _ => return None, // not an infix operator }; - 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 - }, - }) + Some((spacing, index)) } let Some(right_tightness) = tightness(right) else { @@ -367,12 +361,6 @@ 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 { @@ -582,15 +570,13 @@ 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, @@ -598,10 +584,6 @@ 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), @@ -610,17 +592,16 @@ fn prefix(p: &mut Parser) -> Closed { TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus => unary(p), - TokenKind::Not => unary(p), + TokenKind::Minus | TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), _ => { assert!( - !is_prefix_token(p.peek_with_spaces()), - "{:?} is not a prefix token", - p.peek_with_spaces() + !PREFIX_TOKENS.contains(p.peek()), + "{:?} found in PREFIX_TOKENS", + p.peek() ); let span = p.span(); @@ -634,9 +615,9 @@ fn prefix(p: &mut Parser) -> Closed { } fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { - let (kind, spaces) = op; - match kind { + match op.0 { TokenKind::Plus + | TokenKind::Minus | TokenKind::Star | TokenKind::Slash | TokenKind::EqualEqual @@ -645,14 +626,11 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual - | TokenKind::Colon - | TokenKind::Dot - | TokenKind::Pipe => infix_binary(p, op), - TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op), + | TokenKind::Colon => infix_binary(p, op), TokenKind::Equal => infix_let(p, op), - _ if is_prefix_token(op) => infix_call(p, op), + _ if PREFIX_TOKENS.contains(op.0) => infix_call(p, op), _ => panic!("unhandled infix operator {op:?}"), } @@ -672,7 +650,7 @@ fn infix_binary(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { } fn infix_call(p: &mut Parser, mut arg: (TokenKind, Spaces)) -> NodeKind { - while is_prefix_token(p.peek_with_spaces()) { + while PREFIX_TOKENS.contains(p.peek()) { precedence_parse(p, arg); arg = p.peek_with_spaces(); } diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index a005630..c613408 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -40,8 +40,6 @@ pub enum TokenKind { Colon, Backslash, RArrow, - Dot, - Pipe, // Keywords Underscore, @@ -137,10 +135,6 @@ 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 3d49a8b..a68dc93 100644 --- a/crates/haku2/build.rs +++ b/crates/haku2/build.rs @@ -46,7 +46,6 @@ 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 136f21f..ec428fc 100644 --- a/crates/haku2/src/haku2.zig +++ b/crates/haku2/src/haku2.zig @@ -22,7 +22,6 @@ 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 3269a60..d1498ff 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 b.lt(a); + return !a.eql(b) and !(a.lt(b) orelse return null); } pub fn typeName(value: Value) []const u8 { @@ -69,24 +69,24 @@ pub const Value = union(enum) { }; } - pub fn format(value: Value, writer: *std.Io.Writer) !void { + pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { switch (value) { - .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 }), + .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 }), .ref => |ref| switch (ref.*) { - .closure => |c| try writer.print("function {s}", .{c.name}), + .closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}), .list => |l| { - try writer.writeAll("["); + try std.fmt.formatBuf("[", options, writer); for (l, 0..) |elem, i| { - if (i != 0) try writer.writeAll(", "); - try elem.format(writer); + if (i != 0) try std.fmt.formatBuf(", ", options, writer); + try elem.format(fmt, options, writer); } - try writer.writeAll("]"); + try std.fmt.formatBuf("]", options, writer); }, - inline .shape, .scribble, .reticle => |x| try writer.print("{}", .{x}), + inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}), }, } } diff --git a/crates/haku2/src/vm.zig b/crates/haku2/src/vm.zig index 7bec9a1..9f90953 100644 --- a/crates/haku2/src/vm.zig +++ b/crates/haku2/src/vm.zig @@ -147,24 +147,6 @@ 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 { @@ -185,7 +167,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 {f} <- {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), val }); + log.debug("PUSH {any} <- {}", .{ vm.stack[0..vm.stack_top], val }); vm.stack[vm.stack_top] = val; vm.stack_top += 1; } @@ -194,7 +176,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 {f} -> {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), result }); + log.debug("POP {any} -> {}", .{ vm.stack[0..vm.stack_top], result }); return vm.stack[vm.stack_top]; } diff --git a/docs/haku.dj b/docs/haku.dj deleted file mode 100644 index aa93e43..0000000 --- a/docs/haku.dj +++ /dev/null @@ -1,520 +0,0 @@ -# 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 6c89a89..7335554 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -1,15 +1,5 @@ # 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. @@ -38,7 +28,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. @@ -96,8 +86,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) ] ``` @@ -118,16 +108,26 @@ 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,7 +179,6 @@ 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. @@ -187,7 +186,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)`. @@ -198,22 +197,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 + vec 10 0` expression is parenthesized. +Also note how the `d To` 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! @@ -232,10 +231,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 @@ -250,74 +249,16 @@ 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) (vec 32 32)) + stroke 8 #F00 (circle (d To + vec (-16) 0) 16) + stroke 8 #00F (rect (d To + vec 0 (-16)) 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. @@ -329,7 +270,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) ] ``` @@ -340,7 +281,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) --- ] ``` @@ -353,8 +294,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) + -- ] ``` @@ -381,7 +322,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) --------- ] ``` @@ -414,7 +355,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) --------- ] ``` @@ -430,7 +371,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 is the name of the piece of data we're extracting from `d`. +It represents 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. @@ -447,7 +388,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) ] ``` @@ -461,7 +402,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) ] ``` @@ -623,7 +564,6 @@ Seriously, 64 is my limit. I wonder if there's any way we could automate this? - ### The Ouroboros You know the drill by now. @@ -647,7 +587,7 @@ splat: \d, radius -> airbrush: \d, size -> [ splat d size - airbrush d size-8 + airbrush d (size - 8) ] withDotter \d -> @@ -709,7 +649,7 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d size-8 + airbrush d (size - 8) ] else [] @@ -735,8 +675,8 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d size-1 - --- + airbrush d (size - 1) + --- ] else [] @@ -756,7 +696,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 21bc332..5cd488c 100644 --- a/rkgk.toml +++ b/rkgk.toml @@ -23,7 +23,6 @@ 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 bf170d6..6fc9acd 100644 --- a/static/base.css +++ b/static/base.css @@ -53,7 +53,6 @@ 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 bee6be6..dfcc850 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,10 +40,14 @@ thickness: 4 length: 5 duty: 0.5 +or_: \\a, b -> + if (a) a + else b + withDotter \\d -> - visible? = d.Num |mod length < length * duty + visible? = mod (d Num) length < length * duty if (visible?) - stroke thickness color (line d.From d.To) + stroke thickness color (line (d From) (d To)) else () `.trim(), @@ -57,7 +61,7 @@ color: #0003 thickness: 6 withDotter \\d -> - stroke thickness color (line d.From d.To) + stroke thickness color (line (d From) (d To)) `.trim(), }, @@ -72,10 +76,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(), }, @@ -89,21 +93,22 @@ 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 - clockwise = norm (perpClockwise d.To-d.From) * vec a a - from = d.From + clockwise - to = d.To + clockwise + 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 stroke thickness color (line from to) `.trim(), }, @@ -116,16 +121,16 @@ wavelength: 0.1 thickness: 8 colorCurve: \\n -> - n |cos |abs + abs (cos n) 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(), },