Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f9f29fe
wip: start FormulaGrid visualizer
gwhitney Feb 19, 2025
da52848
feat: Prototype FormulaGrid visualizer
gwhitney Feb 21, 2025
c00838e
feat: prototype mouseover feature of FormulaGrid
gwhitney Feb 22, 2025
1739a31
fix: correct aspect validation
gwhitney Feb 23, 2025
da6cdab
fix: interpret any number as a color in some way or other
gwhitney Feb 23, 2025
55aadac
fix: Typescript doesn't know abs(number) is number??
gwhitney Feb 23, 2025
a62a326
fix: Miscellaneous UI improvements
gwhitney Feb 23, 2025
82ae456
feat: mouseover text opaque, shadowed, and moves left to right
gwhitney Feb 24, 2025
8b4e66f
feat: inset is formula
gwhitney Feb 24, 2025
b7e72fb
fix: edge browser z-indexing
gwhitney Feb 24, 2025
f00cd6a
fix: updates per review
gwhitney Feb 24, 2025
c5681b6
doc: add diagram for hexagon and plug for using it with Triangle fill…
gwhitney Feb 25, 2025
09a9032
featured specs
katestange Feb 25, 2025
0e3495e
doc: triangle diagram and misc fixes
gwhitney Feb 25, 2025
49316b4
feat: mouseover popup travels with mouse pointer, avoiding in corner
gwhitney Mar 2, 2025
e605985
doc,test: document and test math.triangular and inverse
gwhitney Mar 2, 2025
5c48ccc
fix,test: remove async race in Scope.vue mounting, update tests
gwhitney Mar 3, 2025
07024cb
fix: Recompute size on visualizer reset(); e.g, aspect ratio might ch…
gwhitney Mar 4, 2025
6f68486
ui: placeholder for aspect
gwhitney Mar 6, 2025
ebf41de
ui: ditto for dimensions
gwhitney Mar 6, 2025
0b9dd74
feat: allow placeholder to remain on input, use for dimensions/aspect
gwhitney Mar 7, 2025
4d978a3
chore: briefer formulas in featured gallery and generate example images
gwhitney Mar 8, 2025
dfe856f
doc: final polish on new doc, some formula features for it
gwhitney Mar 11, 2025
e33b9db
chore: continuous integration snapshots
gwhitney Mar 11, 2025
cdc93d3
doc: fix capitalization typo
gwhitney Mar 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified e2e/tests/featured.spec.ts-snapshots/BeattyDNA-chromium-linux.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/img/FormulaGrid/Hexagons.png
Binary file added src/assets/img/FormulaGrid/Triangles.png
Binary file added src/assets/img/FormulaGrid/UlamSpiral.png
143 changes: 119 additions & 24 deletions src/components/ParamField.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<div>
<div class="param-field">
<label>
<label class="param-label">
{{ displayName }}
<input
v-if="param.type === ParamType.BOOLEAN"
Expand Down Expand Up @@ -36,7 +36,6 @@
:class="!status.isValid() ? 'error-field' : ''"
:value="value"
:placeholder="placehold(param)"
@keyup.enter="growArea($event)"
@input="updateString($event)" />
<input
v-else
Expand All @@ -47,15 +46,24 @@
:placeholder="placehold(param)"
@keyup.enter="blurField($event)"
@input="updateString($event)">
<div
v-if="param.placeholderAlways"
v-safe-html="param.placeholder"
class="fakePlaceholder" />
</label>

<div
v-if="param.hideDescription && param.description"
class="desc-tooltip">
<span class="material-icons-sharp">help</span>
<div class="desc-tooltip-text shadowed">
{{ param.description }}
</div>
<span
class="material-icons-sharp"
@mouseenter="popHelp"
@mouseleave="hideHelp">help</span>
<Teleport to="body">
<div ref="helpText" class="desc-tooltip-text shadowed">
{{ param.description }}
</div>
</Teleport>
</div>
</div>
<p
Expand All @@ -76,7 +84,7 @@
</template>

<script setup lang="ts">
import {ref, watch} from 'vue'
import {ref, onMounted, useTemplateRef, watch} from 'vue'
import PickColors from 'vue-pick-colors'

import type {ParamInterface} from '../shared/Paramable'
Expand All @@ -91,6 +99,33 @@
status: ValidationStatus
}>()

const helpPopup = useTemplateRef('helpText')
function popHelp(e: Event) {
const help = helpPopup?.value
if (!help) return
help.style.opacity = '1'
help.style.visibility = 'visible'
if (e.target instanceof HTMLSpanElement) {
const rect = e.target.getBoundingClientRect()
const popHeight = help.offsetHeight
if (rect.top > popHeight) {
help.style.top = rect.top - popHeight - 4 + 'px'
help.style.right = '8px'
} else if (rect.bottom + popHeight + 4 < window.innerHeight) {
help.style.top = rect.bottom + 4 + 'px'
help.style.right = '8px'
} else {
help.style.top = '0px'
help.style.right = '42px'
}
}
}
function hideHelp() {
if (!helpPopup?.value) return
helpPopup.value.style.opacity = '0'
helpPopup.value.style.visibility = 'hidden'
}

const emit = defineEmits(['updateParam'])
const isColorful =
props.param.type === ParamType.COLOR
Expand All @@ -112,14 +147,59 @@
const field = e.target
if (field instanceof HTMLElement) field.blur()
}
function growArea(e: Event) {
const area = e.target
if (area instanceof HTMLTextAreaElement) {
const curheight = area.getBoundingClientRect().height
area.style.height = `${curheight + 14}px`

function growArea(area: HTMLTextAreaElement) {
if (area.scrollHeight > area.offsetHeight) {
area.style.height = `${area.scrollHeight + 3}px`
}
}

// based on: https://stackoverflow.com/questions/118241
// ---------
function cssStyle(element: HTMLElement, prop: string) {
return window.getComputedStyle(element, null).getPropertyValue(prop)
}

function canvasFont(el: HTMLElement) {
const fontWeight = cssStyle(el, 'font-weight') || 'normal'
const fontSize = cssStyle(el, 'font-size') || '16px'
const fontFamily = cssStyle(el, 'font-family') || 'Times New Roman'

return `${fontWeight} ${fontSize} ${fontFamily}`
}

const rulerContext = document.createElement('canvas').getContext('2d')
function textWidth(text: string, element: HTMLElement) {
if (!rulerContext) return 10
rulerContext.font = canvasFont(element)
return rulerContext.measureText(text).width
}
// ---------

function repositionFake(t: HTMLInputElement | HTMLTextAreaElement) {
const faker = t.parentElement?.querySelector('.fakePlaceholder')
if (faker instanceof HTMLElement) {
faker.style.left = textWidth(t.value, t) + 12 + 'px'
}
}

onMounted(() => {
const field = document.getElementById(props.paramName)
if (
props.param.type === ParamType.FORMULA
&& field instanceof HTMLTextAreaElement
) {
growArea(field)
}
if (
(props.param.placeholderAlways
&& field instanceof HTMLInputElement)
|| field instanceof HTMLTextAreaElement
) {
repositionFake(field)
}
})

function updateBoolean(e: Event) {
blurField(e)
const inp = e.target as HTMLInputElement
Expand All @@ -134,6 +214,13 @@
|| t instanceof HTMLTextAreaElement
) {
emit('updateParam', t.value)
if (t instanceof HTMLTextAreaElement) growArea(t)
if (
!(t instanceof HTMLSelectElement)
&& props.param.placeholderAlways
) {
repositionFake(t)
}
}
}

Expand Down Expand Up @@ -218,6 +305,7 @@
}

function placehold(par: ParamInterface<ParamType>) {
if (par.placeholderAlways) return ''
if (typeof par.placeholder === 'string') return par.placeholder
const stringifier = typeFunctions[par.type].derealize
return stringifier.call(par, par.default as never)
Expand All @@ -229,6 +317,10 @@
font-size: 12px;
}

.param-label {
position: relative;
}

input {
&[type='text'] {
border: none;
Expand All @@ -247,7 +339,8 @@
}
}

::placeholder {
::placeholder,
.fakePlaceholder {
color: grey;
opacity: 0.5;
}
Expand All @@ -260,6 +353,14 @@
opacity: 0.5;
}

.fakePlaceholder {
position: absolute;
font-size: 14px;
left: 10px;
bottom: 8px;
z-index: 2;
}

select {
border: 1px solid var(--ns-color-black);
background-color: var(--ns-color-white);
Expand Down Expand Up @@ -327,11 +428,10 @@
}

right: 4px;
bottom: 0px;
top: 4px;
}

.desc-tooltip .desc-tooltip-text {
visibility: hidden;
.desc-tooltip-text {
width: 240px;
background-color: var(--ns-color-white);
color: var(--ns-color-black);
Expand All @@ -340,20 +440,15 @@
padding: 8px;
font-size: 12px;

position: absolute;
z-index: 1;
bottom: 125%;
position: fixed;
z-index: 120;
right: 0;
margin-left: -120px;

opacity: 0;
visibility: hidden;
transition:
opacity 0.2s,
visibility 0.2s;
}

.desc-tooltip:hover .desc-tooltip-text {
visibility: visible;
opacity: 1;
}
</style>
54 changes: 39 additions & 15 deletions src/shared/Chroma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ the [chroma-js api](https://www.vis4.net/chromajs/). Additional functions
and facilities for manipulating Chroma colors are documented below.

All of the chroma-js api and operations documented here are also available
in [mathjs formulas](math.md). In addition, all of the named colors (like
`red` or `chartreuse`) are available as pre-defined constant symbols, as
are the color brewer palettes, like `RdBu` or `Set1`. Note the palettes are
arrays of colors, so to get a specific color from them in a formula you
need to index them with a 1-based index, e.g., `Set1[5]`.
in [mathjs formulas](math.md). For example, you can darken a color `x` an
amount controlled by a number `x` by writing `c.darken(x)`, or desaturate it
by writing `c.desaturate(x)`, etc.

In addition, all of the named colors (like
`red` or `chartreuse`, including all CSS (Cascading Style Sheets) named colors)
are available as pre-defined constant symbols, as are the color brewer
palettes, like `RdBu` or `Set1`. Note the palettes are arrays of colors,
so to get a specific color from them in a formula you need to index them
with a 1-based index, e.g., `Set1[5]`.
**/
import type {Color as Chroma} from 'chroma-js'
import chromaRaw from 'chroma-js'
Expand Down Expand Up @@ -59,11 +64,29 @@ export const chroma = function (...args: unknown[]) {
if (arg instanceof Array && arg.length === 4) {
return chromaRaw(...(arg as Quad), 'gl')
}
if (typeof arg === 'boolean') {
return arg ? chromaRaw('white') : chromaRaw('black').alpha(0)
}
if (typeof arg === 'number') {
if (arg <= 1.0) {
return chromaRaw(arg, arg, arg, 1, 'gl')
// Can't think of any natural meaning for negative numbers,
// so just ignore sign
const n: number = Math.abs(arg)
if (n <= 1.0) {
return chromaRaw(n, n, n, 1, 'gl')
}
if (n > 0xffffff) {
// largest number chroma interprets
if (n < 0xffffffff) {
// interpret last two digits as alpha
const alpha = n % 0x100
const rest = Math.floor(n / 0x100)
return chromaRaw(rest).alpha(alpha / 0xff)
} else {
// what color should huge numbers be?
return chromaRaw('white')
}
}
return chromaRaw(arg)
return chromaRaw(n)
}
}
if (
Expand Down Expand Up @@ -104,18 +127,19 @@ const dummy = chroma('white')
const chromaConstructor = dummy.constructor

/** md
#### rainbow(hue: bigint | number)
#### rainbow(hue: bigint | number, opacity?: number)

This function conveniently allows creation of an opaque color from just
a "hue angle" in color space, periodically interpreted with the main period
from 0 to 360. It uses the "oklch" color space provided by chromajs to ensure
that all of the colors it returns will have approximately the same apparent
lightness and hue saturation (and hopefully these levels have been selected
to produce attractive colors).
from 0 to 360. Optionally, you can also specify an opacity for the resulting
color, from 1 to 0. It uses the "oklch" color space provided by chromajs
to ensure that all of the colors it returns will have approximately the same
apparent lightness and hue saturation (and hopefully these levels have been
selected to produce attractive colors).
**/
export function rainbow(hue: bigint | number): Chroma {
export function rainbow(hue: bigint | number, opacity = 1): Chroma {
if (typeof hue === 'bigint') hue = Number(hue % 360n)
return chroma.oklch(0.6, 0.25, hue % 360)
return chroma.oklch(0.6, 0.25, hue % 360).alpha(opacity)
}

/** md
Expand Down
12 changes: 8 additions & 4 deletions src/shared/ParamType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ function validateExtInt(value: string, status: ValidationStatus) {

//Helper function for color types:
function isColor(value: string) {
return value.trim().match(/^(#[0-9A-Fa-f]{3})|(#[0-9A-Fa-f]{6})$/)
return value
.trim()
.match(/^#?([0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/)
}

export const typeFunctions: {
Expand Down Expand Up @@ -228,15 +230,17 @@ export const typeFunctions: {
)
return
}
const freeVars = fmla.freevars.difference(new Set(inputSymbols))
const knownSymbols = new Set(inputSymbols)
const freeVars = fmla.freevars.difference(knownSymbols)
status.forbid(
freeVars.size,
`free variables limited to ${inputSymbols}; `
+ `please remove '${Array.from(freeVars).join(', ')}'`
)
const freeFuncs = fmla.freefuncs.difference(knownSymbols)
status.forbid(
fmla.freefuncs.size,
`unknown functions '${fmla.freefuncs}'`
freeFuncs.size,
`unknown functions '${Array.from(freeFuncs).join(', ')}'`
)
},
realize: function (value) {
Expand Down
19 changes: 12 additions & 7 deletions src/shared/Paramable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,18 @@ export interface ParamInterface<PT extends ParamType> {
placeholder?: string
/* **/
/** md
: The placeholder text that appears in the input box for the parameter
when that box is empty. This property is really only useful on parameters
for which the `required` property is false, because otherwise the input
box is not allowed to be empty. If the `placeholder` property is not
specified, the string representation of the `default` property is used
instead.
: The placeholder text that appears in the input box for the parameter.
If the `placeholder` property is not specified, the string representation
of the `default` property is used instead.
<!-- -->
**/
/** md */
placeholderAlways?: boolean
/* **/
/** md
: Whether the placeholder should always be displayed, even when something
has been entered in the text box. (The usual behavior is to blank the
placeholder as soon as there is input.)
**/
/** md */
description?: string
Expand Down Expand Up @@ -858,7 +863,7 @@ export class Paramable implements ParamableInterface {
const param = this.params[key]
if (
param.type === ParamType.COLOR
&& value.match(/^[0-9a-fA-F]{6}$/)
&& value.match(/^[0-9a-fA-F]{3,8}$/)
)
this.tentativeValues[key] = '#' + value
else this.tentativeValues[key] = value
Expand Down
Loading