Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Restore Grid visualizer to operation #522

Closed
wants to merge 43 commits into from
Closed
Changes from 1 commit
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1cb5b80
Gallery page (#341)
mbavec Jun 2, 2024
0b9f403
Tabs and reactive visualizer and sequence settings (#342)
kaldis-berzins Jun 26, 2024
4fe297d
Dynamic URLs (#354)
kaldis-berzins Jun 28, 2024
1612c44
Specimen browser storage (#362)
gwhitney Jun 28, 2024
8ebe122
Resizing and url integration [cleaned] (#366)
gwhitney Jun 29, 2024
58ff5ac
feat: Add specimen bar (#367)
gwhitney Jun 30, 2024
f0feac5
Gallery integration [cleaned] (#368)
gwhitney Jul 3, 2024
8b67b98
fix: Restore highlighting of dropzones as tabs are dragged to them (#…
gwhitney Jul 5, 2024
9eee6ef
Sequence and visualizer switcher [cleaned] (#379)
gwhitney Jul 11, 2024
06ab30b
feat: human-readable URLs based on query strings (#389)
gwhitney Jul 16, 2024
f7ee5a8
Restore OEIS sequences, with minor fixes (#395)
gwhitney Jul 23, 2024
63e344a
feat: Allow adding and dropping of OEIS sequence cards (#398)
gwhitney Jul 24, 2024
d352d7a
doc: Update to reflect changes to visualizer class, with concomitant …
katestange Jul 24, 2024
c8f3504
feat: Operational OEIS search bar (#410)
gwhitney Aug 15, 2024
4270b4b
FactorFence Visualizer (#406)
katestange Aug 28, 2024
3ca6998
test: Implement end-to-end testing with Playwright (#420)
gwhitney Oct 10, 2024
3d73aac
doc: Harmonize guidance on git, cloning, and other workflows. (#463)
gwhitney Oct 12, 2024
9ded41c
doc: Two columns in contributor list. Resolves #314. (#464)
gwhitney Oct 12, 2024
a451f8f
feat: Default to a featured visualization (not Random ModFill) (#466)
gwhitney Oct 13, 2024
284735a
Turtle overhaul (#404)
katestange Oct 25, 2024
3ccd392
ui: Move parameter descriptions just below their entry boxes (#472)
gwhitney Oct 25, 2024
b404b7f
refactor: make overall status and status of each parameter part of Pa…
gwhitney Oct 29, 2024
5ab5bbf
fix: Ensure back button updates visualization (#484)
gwhitney Oct 31, 2024
00ff08d
fix: Don't attempt to factor 'unsafe' integers (#485)
gwhitney Oct 31, 2024
e16077b
Documentation site overhaul for alpha (#486)
gwhitney Nov 14, 2024
7f258f5
fix: Don't stretch SpecimenCards when only one row (#495)
gwhitney Nov 14, 2024
1866e20
fix: Don't let one randomly selected featured specimen replace anothe…
gwhitney Nov 15, 2024
aa73fcb
ui: OEIS search fixes and improvements. (#497)
gwhitney Nov 16, 2024
72c41e9
fix[Formula]: reject unknown function at parse to avoid throwing erro…
gwhitney Nov 17, 2024
64cef6a
fix: Drag into sidebar always docks into an empty zone (#499)
gwhitney Nov 20, 2024
5bda8a9
feat: Save a history of any kind of Sequences (not just OEIS) (#500)
gwhitney Nov 24, 2024
1d927f3
feat: add colours to ModFill (#505)
katestange Dec 10, 2024
7a690b0
ui: More "finger pointer" cursors to indicate interactions. (#516)
gwhitney Dec 11, 2024
b275244
feat: Limit WebGL contexts in use by replacing thumbnails with snapsh…
gwhitney Dec 13, 2024
18129b2
fix: Get grid to compile and run without crash; still doesn't work
gwhitney Dec 18, 2024
4792fce
fix: Get Grid to run and produce image, still buggy
gwhitney Dec 19, 2024
dec117d
ui: Streamline presentation of Grid parameters
gwhitney Dec 19, 2024
ac9619b
fix: Don't draw visualizer until show(); draw entire Grid in one frame
gwhitney Dec 21, 2024
ae109c6
fix: Deal with canvas size flexibility
gwhitney Dec 21, 2024
20c7fca
fix: Keep redrawing while there are cache misses
gwhitney Dec 21, 2024
6605a67
feat: transparent property colors
gwhitney Dec 21, 2024
872e7d4
fix: Binary search to find best-fit cell size
gwhitney Dec 22, 2024
baaa648
ui: update property parameters on every change to preset
gwhitney Dec 22, 2024
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
Prev Previous commit
Next Next commit
Resizing and url integration [cleaned] (#366)
* feat: resizing and URL integration

  Changes to tabs
    * Added buttons that let you control the tabs without dragging
    * Support minimizing of tabs.
    * Added a highlight to the tab that is currently selected.
    * Initial mobile compatibility in the style section of Tab.vue

  Resizing
    * Added a reset function to a specimen. This is a hard reset that is used
      when the specimen is resized and it doesn't override resized function.
    * Added a resized function to both specimens and visualizers. It is not
      required for visualizers as they can just not do anything and the
      specimen will hard reset them.

  Scope
    * Adds rudimentary mobile support.

Originally authored by @kaldis-berzins et al.

* fix: Dock back to where you were and non-overlapping initial tab positions

* doc: fix description of docking button

* refactor: Use consistent tab minimized height (and revert icon name change)
gwhitney authored Jun 29, 2024
commit 8ebe12239056c4d989a1de1b23ab6b058d5ffaef
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@

<!-- Material design icons. Use the .material-icons-sharp class to turn text into an icon -->
<link href="https://fonts.googleapis.com/css2?family=Material+Icons+Sharp" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Sharp" rel="stylesheet">

</head>
<body>
<noscript>
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -115,7 +115,7 @@
"prettier": "^3.2.5",
"prettier-eslint": "^16.3.0",
"prettier-eslint-cli": "^8.0.1",
"sass": "^1.77.1",
"sass": "^1.77.2",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vitest": "^1.5.0",
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -60,6 +60,9 @@
/* Bolditude */
--ns-font-weight-medium: 500;

/* Dimensions */
--ns-desktop-tab-width: 300px;

/* Breakpoint widths
Default styles should be for vertical mobile devices
(devices narrower than --ns-breakpoint-mobile)
281 changes: 249 additions & 32 deletions src/components/Tab.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
<template>
<div class="tab">
<div class="drag"></div>
<div class="tab" @click="selected">
<div class="drag">
<div class="buttons">
<span
class="minimize material-icons-sharp"
@click="minMaxWindow">
minimize
</span>
<span
class="docking material-symbols-sharp"
@click="dockWindow">
dock_to_right
</span>
</div>
</div>
<div class="content">
<slot></slot>
</div>
@@ -10,7 +23,13 @@

<script setup lang="ts">
import interact from 'interactjs'
import {positionAndSizeTab} from '../views/Scope.vue'
import {
positionAndSizeTab,
positionAndSizeAllTabs,
selectTab,
} from '../views/Scope.vue'
const minimizedTabHeight = 110
// every element with draggable class can be dragged
interact('.tab').resizable({
@@ -41,8 +60,19 @@
if (
tab instanceof HTMLElement
&& !tab.classList.contains('docked')
)
) {
// select the tab when it is resized
selectTab(tab)
// update the classlist with "minimized"
// if the height is less or equal than the
// minimized tab height
tab.style.height = event.rect.height + 'px'
if (event.rect.height <= minimizedTabHeight) {
tab.classList.add('minimized')
} else {
tab.classList.remove('minimized')
}
}
},
},
modifiers: [
@@ -53,7 +83,7 @@
// minimum size
interact.modifiers.restrictSize({
min: {width: 0, height: 128},
min: {width: 700, height: 90},
}),
],
})
@@ -63,9 +93,8 @@
autoScroll: false,
listeners: {
start: (event: Interact.InteractEvent) => {
start: () => {
document.body.style.userSelect = 'none'
event.target.parentElement!.style.zIndex += 10
},
move: dragMoveListener,
@@ -77,13 +106,15 @@
const tab = event.target.parentElement
if (!(tab instanceof HTMLElement)) return
if (tab.getAttribute('docked') === 'none') return
const zoneName = tab.getAttribute('docked')
if (!zoneName || zoneName === 'none') return
const dropzone = document.querySelector(
'#' + tab.getAttribute('docked') + '-dropzone'
`#${zoneName}-dropzone`
)
if (!(dropzone instanceof HTMLElement)) return
tab.setAttribute('last-dropzone', zoneName)
positionAndSizeTab(tab, dropzone)
},
},
@@ -96,6 +127,8 @@
target instanceof HTMLElement
&& container instanceof HTMLElement
) {
selectTab(target)
const containerRect = container.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
// keep position in attributes
@@ -122,40 +155,224 @@
target.setAttribute('data-y', boundedY.toString())
}
}
</script>
function minMaxWindow(event: MouseEvent) {
const tab = (event.currentTarget as HTMLElement).closest('.tab')
if (!(tab instanceof HTMLElement)) return
<style scoped lang="scss">
.tab {
border: 1px solid var(--ns-color-black);
width: 300px;
height: 200px;
z-index: 50;
selectTab(tab)
const content = tab.querySelector('.content')
if (!(content instanceof HTMLElement)) return
// if the tab is docked, we have a different behavior
if (tab.classList.contains('docked')) {
// vertical and horizontal position of the tab
// (eg. top-right, where vert is top and side is right)
const vert = tab.getAttribute('docked')?.split('-')[0]
const side = tab.getAttribute('docked')?.split('-')[1]
// get the correct dropzone wrapper
const dropzoneWrapper = tab.parentElement?.querySelector(
'#' + side + '-dropzone-container'
)?.firstChild
if (!(dropzoneWrapper instanceof HTMLElement)) return
if (dropzoneWrapper instanceof HTMLElement) {
if (tab.classList.contains('minimized')) {
// if we want to maximize top tab,
// set height of wrapper to 400px,
// if we want to maximize bottom tab,
// set height (of top tab wrapper) to 100% - 400px
if (vert === 'top') {
dropzoneWrapper.style.height = '400px'
} else {
dropzoneWrapper.style.height = 'calc(100% - 400px)'
}
content.style.overflowY = 'scroll'
tab.classList.remove('minimized')
// update the size and position of all tabs
positionAndSizeAllTabs()
} else {
// if we want to minimize top tab,
// set height of wrapper to the minimized tab height,
// if we want to minimize bottom tab,
// set height (of top tab wrapper) to 100% - XXXpx,
// where XXX is a bit less than the minimized tab height
if (vert === 'top') {
dropzoneWrapper.style.height = `${minimizedTabHeight}px`
} else {
// Temp variable to beat lint's line length limits :(
const h = `calc(100% - ${minimizedTabHeight - 20}px)`
dropzoneWrapper.style.height = h
}
content.style.overflowY = 'hidden'
tab.classList.add('minimized')
dropzoneWrapper.classList.add('resized')
// update the size and position of all tabs
positionAndSizeAllTabs()
}
}
return
}
// If the tab is minimized, maximize it
if (tab.classList.contains('minimized')) {
tab.style.height = '400px'
content.style.overflowY = 'scroll'
tab.classList.remove('minimized')
return
}
// If the tab is maximized, minimize it
else {
tab.style.height = '90px'
content.style.overflowY = 'hidden'
tab.classList.add('minimized')
}
}
// helper to choose docking site
function zoneOrder(preferred: string) {
const zoneParts = preferred.split('-')
const otherVert = zoneParts[0] === 'top' ? 'bottom' : 'top'
return [preferred, `${otherVert}-${zoneParts[1]}`]
}
.resize {
height: 16px;
width: 100%;
position: absolute;
bottom: 0;
function dockWindow(event: MouseEvent) {
const tab = (event.currentTarget as HTMLElement).closest('.tab')
if (!(tab instanceof HTMLElement)) return
// if the tab is docked, different behavior
if (tab.classList.contains('docked')) {
// get the last undocked position of the tab
const x =
(tab.getAttribute('last-coords-x') || 0).toString() + 'px'
const y =
(tab.getAttribute('last-coords-y') || 0).toString() + 'px'
// move the tab to the last undocked position
tab.style.left = x
tab.style.top = y
// update attributes
tab.setAttribute('data-x', x)
tab.setAttribute('data-y', y)
// update the classlist with "docked" if the tab is docked
const dropzone = document.querySelector(
'#' + tab.getAttribute('docked') + '-dropzone'
)
const dropzoneContainer = dropzone?.parentElement?.parentElement
if (
tab instanceof HTMLElement
&& dropzone instanceof HTMLElement
&& dropzoneContainer instanceof HTMLElement
&& tab.classList.contains('docked')
) {
// update classlists when undocking
dropzone.classList.add('empty')
tab.classList.remove('docked')
tab.setAttribute('docked', 'none')
// if both dropzones are empty,
// make the dropzone container empty aswell
if (
dropzoneContainer.querySelectorAll('.empty').length == 2
) {
dropzoneContainer.classList.add('empty')
}
}
return
}
selectTab(tab)
// get current position
const x = parseFloat(tab.getAttribute('data-x') || '0')
const y = parseFloat(tab.getAttribute('data-y') || '0')
// save current position before docking
tab.setAttribute('last-coords-x', x.toString())
tab.setAttribute('last-coords-y', y.toString())
const preferredZone = tab.getAttribute('last-dropzone') || 'top-right'
const zonesToTry = zoneOrder(preferredZone)
for (const zoneName of zonesToTry) {
const zone = document.querySelector(`#${zoneName}-dropzone`)
if (!(zone instanceof HTMLElement)) continue
if (zone.classList.contains('empty')) {
positionAndSizeTab(tab, zone)
tab.setAttribute('last-dropzone', zoneName)
break
}
}
}
// select the tab when it is clicked
function selected(event: MouseEvent) {
const tab = (event.currentTarget as HTMLElement).closest('.tab')
if (!(tab instanceof HTMLElement)) return
// The drag element is actually underneath the entire window
// This is so that dropping is more intuitive
.drag {
position: absolute;
height: 100%;
width: 100%;
background-color: var(--ns-color-black);
selectTab(tab)
}
</script>

<style scoped lang="scss">
// mobile styles
.buttons {
display: none;
}
.tab {
width: 300px;
height: fit-content;
}
.content {
padding: 16px;
position: absolute;
top: 16px;
background-color: var(--ns-color-white);
width: 100%;
height: calc(100% - 16px);
overflow-y: scroll;
overflow-x: hidden;
max-width: 500px;
}
// desktop styles
@media (min-width: 700px) {
.buttons {
display: flex;
justify-content: flex-end;
gap: 8px;
padding-right: 8px;
padding-top: 2px;
padding-bottom: 2px;
}
.minimize,
.docking {
cursor: pointer;
color: var(--ns-color-white);
font-size: 12px;
}
.tab {
border: 1px solid var(--ns-color-black);
width: var(--ns-desktop-tab-width);
height: 200px;
z-index: 50;
}
.resize {
height: 16px;
width: 100%;
position: absolute;
bottom: 0;
}
// The drag element is actually underneath the entire window
// This is so that dropping is more intuitive
.drag {
position: absolute;
height: 100%;
width: 100%;
background-color: var(--ns-color-black);
}
.content {
padding: 16px;
position: absolute;
top: 16px;
background-color: var(--ns-color-white);
width: 100%;
height: calc(100% - 16px);
overflow-y: scroll;
overflow-x: hidden;
}
}
</style>
84 changes: 81 additions & 3 deletions src/shared/Specimen.ts
Original file line number Diff line number Diff line change
@@ -33,6 +33,7 @@ export class Specimen {
private _sequence: SequenceInterface<GenericParamDescription>
private location?: HTMLElement
private isSetup: boolean = false
private size: {width: number; height: number}

/**
* Constructs a new specimen from a visualizer and a sequence.
@@ -56,18 +57,40 @@ export class Specimen {
this._visualizer = new vizMODULES[visualizerKey].visualizer(
this._sequence
)
this.size = {width: 0, height: 0}
}
/**
* Call this as soon after construction as possible once the HTML
* element has been mounted
*/
setup(location: HTMLElement) {
this.location = location
this._visualizer.view(this._sequence)
this._visualizer.inhabit(this.location)
this._visualizer.show()
this.size = this.calculateSize(
this.location.clientWidth,
this.location.clientHeight,
this.visualizer.requestedAspectRatio()
)

this.visualizer.view(this.sequence)
this.visualizer.inhabit(this.location, this.size)
this.visualizer.show()
this.isSetup = true
}
/**
* Hard resets the specimen
*/
reset() {
if (!this.location) return
this.size = this.calculateSize(
this.location.clientWidth,
this.location.clientHeight,
this.visualizer.requestedAspectRatio()
)

this.visualizer.depart(this.location)
this.visualizer.inhabit(this.location, this.size)
this.visualizer.show()
}
/**
* Returns the name of this specimen
*/
@@ -140,6 +163,61 @@ export class Specimen {
updateSequence() {
this.visualizer.view(this.sequence)
}

/**
* Calculates the size of the visualizer in its container.
* @param containerWidth width of the container
* @param containerHeight height of the container
* @param aspectRatio aspect ratio requested by visualizer
* @returns the size of the visualizer
*/
calculateSize(
containerWidth: number,
containerHeight: number,
aspectRatio: number | undefined
): {width: number; height: number} {
if (aspectRatio === undefined)
return {
width: containerWidth,
height: containerHeight,
}
const constraint = containerWidth / containerHeight < aspectRatio
return {
width: constraint
? containerWidth
: containerHeight * aspectRatio,
height: constraint
? containerWidth / aspectRatio
: containerHeight,
}
}
/**
* This function should be called when the size of the visualizer container
* has changed. It calculates the size of the contents according to the
* aspect ratio requested and calls the resize function.
* @param width New width of the visualizer container
* @param height New height of the visualizer container
*/
resized(width: number, height: number): void {
const newSize = this.calculateSize(
width,
height,
this.visualizer.requestedAspectRatio()
)

if (
this.size.width === newSize.width
&& this.size.height === newSize.height
)
return

this.size = newSize

if (!this.visualizer.resized?.(this.size.width, this.size.height)) {
// Reset the visualizer if the resized function isn't implemented
this.reset()
}
}
/**
* Converts the specimen to a URL as a way of saving all information
* about the specimen.
10 changes: 4 additions & 6 deletions src/views/Gallery.vue
Original file line number Diff line number Diff line change
@@ -6,12 +6,7 @@
<h3>Self similarity telescope</h3>
</div>
<div type="button" id="change-button">
<span
alt="Swap icon"
id="change-icon"
class="material-icons-sharp"
>swap_horiz</span
>
<span class="material-icons-sharp">swap_horiz</span>
<p id="change-text">Select Visualizer</p>
</div>
</div>
@@ -87,6 +82,9 @@
#change-button {
max-width: 100px;
width: min-content;
display: flex;
flex-direction: column;
align-items: center;
}
#change-icon {
display: block;
215 changes: 175 additions & 40 deletions src/views/Scope.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<div id="specimen-container">
<tab id="sequenceTab" class="tab docked" docked="top-right">
<tab
id="sequenceTab"
class="tab docked"
docked="top-right"
:last-coords-x="Math.floor(tabWidth / 3)"
:last-coords-y="Math.floor(tabWidth / 3)">
<ParamEditor
title="Sequence"
:paramable="specimen.sequence"
@@ -11,7 +16,13 @@
}
" />
</tab>
<tab id="visualiserTab" class="tab docked" docked="bottom-right">
<tab
id="visualiserTab"
class="tab docked"
docked="bottom-right"
last-dropzone="bottom-right"
:last-coords-x="Math.floor((2 * tabWidth) / 3)"
:last-coords-y="Math.floor((2 * tabWidth) / 3)">
<ParamEditor
title="Visualizer"
:paramable="specimen.visualizer"
@@ -79,6 +90,9 @@
tab: HTMLElement,
dropzone: HTMLElement
): void {
if (window.innerWidth < 700) return
const dropzoneContainer = dropzone.parentElement?.parentElement
const dropzoneRect = dropzone.getBoundingClientRect()
const {x, y} = translateCoords(dropzoneRect.x, dropzoneRect.y)
@@ -87,8 +101,32 @@
tab.style.left = x + 'px'
tab.style.height = dropzoneRect.height + 'px'
// update the classlist with "minimized"
// if the height is less or equal than 110
if (dropzoneRect.height <= 110) {
tab.classList.add('minimized')
} else {
tab.classList.remove('minimized')
}
tab.setAttribute('data-x', x.toString())
tab.setAttribute('data-y', y.toString())
if (
tab instanceof HTMLElement
&& dropzoneContainer instanceof HTMLElement
&& dropzone instanceof HTMLElement
&& dropzone.classList.contains('empty')
) {
dropzone.classList.remove('empty')
dropzone.classList.remove('drop-hover')
dropzoneContainer.classList.remove('empty')
tab.classList.add('docked')
const dropzoneAttribute = dropzone.getAttribute('dropzone')
if (dropzoneAttribute !== null) {
tab.setAttribute('docked', dropzoneAttribute)
}
}
}
/**
@@ -121,7 +159,7 @@
* Doesn't affect non-docked tabs.
* Used when the window is resized.
*/
function positionAndSizeAllTabs(): void {
export function positionAndSizeAllTabs(): void {
document.querySelectorAll('.tab').forEach((tab: Element) => {
if (!(tab instanceof HTMLElement)) return
if (tab.getAttribute('docked') === 'none') return
@@ -134,6 +172,29 @@
positionAndSizeTab(tab, dropzone)
})
}
// selects a tab
export function selectTab(tab: HTMLElement): void {
deselectTab()
const drag = tab.querySelector('.drag')
if (!(drag instanceof HTMLElement)) return
drag.classList.add('selected')
drag.style.backgroundColor = 'var(--ns-color-primary)'
tab.style.zIndex = '100'
}
// deselects all tabs
export function deselectTab(): void {
const tabs = document.querySelectorAll('.tab')
tabs.forEach(tab => {
if (tab instanceof HTMLElement) {
const drag = tab.querySelector('.drag')
if (!(drag instanceof HTMLElement)) return
drag.classList.remove('selected')
drag.style.backgroundColor = 'var(--ns-color-black)'
tab.style.zIndex = '1'
}
})
}
</script>

<script setup lang="ts">
@@ -156,6 +217,12 @@
: defaultSpecimen
)
const tabWidth = parseInt(
window
.getComputedStyle(document.documentElement)
.getPropertyValue('--ns-desktop-tab-width')
)
const updateURL = () =>
router.push({
query: {
@@ -173,7 +240,15 @@
window.addEventListener('resize', () => {
positionAndSizeAllTabs()
})
specimen.setup(canvasContainer)
setInterval(() => {
specimen.resized(
canvasContainer.clientWidth,
canvasContainer.clientHeight
)
}, 500)
})
// enable draggables to be dropped into this
@@ -272,7 +347,7 @@
dropzoneWrapper.style.height =
Math.min(
event.rect.height,
dropContRect.height - 144
dropContRect.height - 90
) + 'px'
dropzoneWrapper.classList.add('resized')
positionAndSizeAllTabs()
@@ -287,13 +362,14 @@
// minimum size
interact.modifiers.restrictSize({
min: {width: 0, height: 128},
min: {width: 700, height: 90},
}),
],
})
</script>

<style scoped lang="scss">
// mobile styles
#specimen-container {
height: calc(100vh - 54px);
position: relative;
@@ -307,64 +383,123 @@
flex: 1;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.dropzone-container {
display: flex;
flex-direction: column;
width: 300px;
height: 100%;
&#right-dropzone-container {
right: 0;
top: 0;
min-height: fit-content;
padding-left: auto;
padding-right: auto;
}
.dropzone-container {
display: none;
}
#canvas-container {
order: 1;
border-bottom: 1px solid var(--ns-color-black);
height: 301px;
}
#sequenceTab {
width: 100%;
padding-left: auto;
padding-right: auto;
order: 2;
border-bottom: 1px solid var(--ns-color-black);
}
#visualiserTab {
width: 100%;
padding-left: auto;
padding-right: auto;
order: 3;
border-bottom: 1px solid var(--ns-color-black);
}
// desktop styles
@media (min-width: 700px) {
#sequenceTab,
#visualiserTab {
width: var(--ns-desktop-tab-width);
}
&#left-dropzone-container {
left: 0;
top: 0;
#specimen-container {
height: calc(100vh - 54px);
position: relative;
}
#main {
display: flex;
height: 100%;
}
&.empty {
position: absolute;
pointer-events: none;
#canvas-container {
height: unset;
.dropzone-resize.material-icons-sharp {
display: none;
}
order: unset;
flex: 1;
position: relative;
overflow: hidden;
}
.dropzone-size-wrapper {
flex-grow: 1;
.dropzone-container {
display: flex;
flex-direction: column;
max-height: calc(100% - 128px);
width: var(--ns-desktop-tab-width);
height: 100%;
&.resized {
flex-grow: unset;
&#right-dropzone-container {
right: 0;
top: 0;
}
.dropzone-resize {
height: 16px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
cursor: ns-resize;
&#left-dropzone-container {
left: 0;
top: 0;
}
&.empty {
position: absolute;
pointer-events: none;
.dropzone-resize.material-icons-sharp {
display: none;
}
}
.dropzone {
.dropzone-size-wrapper {
flex-grow: 1;
display: flex;
flex-direction: column;
max-height: calc(100% - 90px);
&.resized {
flex-grow: unset;
}
&.drop-hover {
background-color: var(--ns-color-primary);
filter: brightness(120%);
.dropzone-resize {
height: 16px;
font-size: 16px;
display: flex;
justify-content: center;
align-items: center;
cursor: ns-resize;
}
.dropzone {
flex-grow: 1;
&.drop-hover {
background-color: var(--ns-color-primary);
filter: brightness(120%);
}
}
}
}
}
.tab {
position: absolute;
.tab {
width: 300px;
position: absolute;
order: unset;
}
}
</style>
8 changes: 8 additions & 0 deletions src/views/__tests__/Gallery.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Gallery from '../Gallery.vue'
import {expect, test} from 'vitest'
import {mount} from '@vue/test-utils'

test('should contain specimen gallery', () => {
const wrapper = mount(Gallery, {shallow: true})
expect(wrapper.html()).toContain('Specimen gallery')
})
39 changes: 12 additions & 27 deletions src/visualizers/P5Visualizer.ts
Original file line number Diff line number Diff line change
@@ -60,6 +60,7 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
{
_sketch?: p5
_canvas?: p5.Renderer
_size: {width: number; height: number}

within?: HTMLElement
get sketch(): p5 {
@@ -86,6 +87,7 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
constructor(seq: SequenceInterface<GenericParamDescription>) {
super(desc)
this.seq = seq
this._size = {width: 0, height: 0}
Object.assign(this, defaultObject)
}

@@ -98,13 +100,18 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
* of the p5 object itself would need to implement an extended or
* replaced inhabit() method.
* @param element HTMLElement Where the visualizer should inject itself
* @param size The width and height the visualizer should occupy
*/
inhabit(element: HTMLElement): void {
inhabit(
element: HTMLElement,
size: {width: number; height: number}
): void {
if (this.within === element) return // already inhabiting there
if (this.within) {
// oops, already inhabiting somewhere else; depart there
this.depart(this.within)
}
this._size = size
this.within = element
this._sketch = new p5(sketch => {
this._sketch = sketch // must assign here, as setup is called
@@ -185,24 +192,14 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
this._sketch?.noLoop()
}

/**
* Determining the maximum pixel width and height the containing
* element allows.
* @returns [number, number] Maximum width and height of inhabited
* element
*/
measure(): [number, number] {
if (!this.within) return [0, 0]
return [this.within.clientWidth, this.within.clientHeight]
}

/**
* The p5 setup for this visualizer. Note that derived Visualizers
* _must_ call this first.
*/
setup() {
const [w, h] = this.measure()
this._canvas = this.sketch.background('white').createCanvas(w, h)
this._canvas = this.sketch
.background('white')
.createCanvas(this._size.width, this._size.height)
}

/**
@@ -217,18 +214,6 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
*/
draw(): void {}

/**
* What to do when the window resizes
*/
windowResized(): void {
if (!this._sketch) return
// Make sure the canvas isn't acting as a "strut" keeping
// the div big:
this._sketch.resizeCanvas(10, 10)
const [w, h] = this.measure()
this._sketch.resizeCanvas(w, h)
}

/**
* Get rid of the visualization altogether
*/
@@ -285,7 +270,7 @@ export function P5Visualizer<PD extends GenericParamDescription>(desc: PD) {
const element = this.within!
this.stop()
this.depart(element)
this.inhabit(element)
this.inhabit(element, this._size)
this.show()
}
}
26 changes: 22 additions & 4 deletions src/visualizers/VisualizerInterface.ts
Original file line number Diff line number Diff line change
@@ -40,12 +40,17 @@ export interface VisualizerInterface<PD extends GenericParamDescription>
* Cause the visualizer to realize itself within a DOM element.
* The visualizer should remove itself from any other location it might
* have been displaying, and prepare to draw within the provided element.
* It is safe to call this with the same element in which
* the visualizer is already displaying.
* It is safe to call this with the same element in which the visualizer
* is already displaying.
* The size provided to the visualizer is the size the visualizer should
* assume, respecting its aspect ratio preferences. If needed, the
* visualizer can also query the size of the element for the full container
* size.
* @param element HTMLElement The DOM node where the visualizer should
* insert itself.
* @param size The width and height the visualizer should occupy
*/
inhabit(element: HTMLElement): void
inhabit(element: HTMLElement, size: {width: number; height: number}): void
/**
* Show the sequence according to this visualizer.
*/
@@ -65,7 +70,20 @@ export interface VisualizerInterface<PD extends GenericParamDescription>
* @param element HTMLElement The DOM node the visualizer was inhabit()ing
*/
depart(element: HTMLElement): void

/**
* This is called when the size of the visualizer should change, either
* because the window is resized, or the docking configuration has changed.
* Visualizer writers should take care to resize their canvas and to make
* sure that any html elements aren't wider than the requested width.
* The new size is the full available size cut down to fit in the requested
* aspect ratio.
*
* Not implementing this function will mean that the visualizer is reset
* on resize
* @param width The new width of the visualizer (in pixels)
* @param height The new height of the visualizer (in pixels)
*/
resized?(width: number, height: number): void
/**
* Provides a way for visualizers to request a specific aspect ratio for
* its canvas. This aspect ratio is specified as a positive n > 0 where