Skip to content

Commit

Permalink
server/tailsql: an indirect note about pronunciation (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
creachadair authored Sep 17, 2023
1 parent 66f4ec1 commit c87a5ec
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 1 deletion.
Binary file added server/tailsql/static/nut.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
44 changes: 44 additions & 0 deletions server/tailsql/static/script.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Params, Area, Cycle, Loop } from './sprite.js';

(() => {
const query = document.getElementById('query');
const qButton = document.getElementById('send-query');
Expand All @@ -7,18 +9,60 @@
const origin = document.location.origin;
const sources = document.getElementById('sources');
const body = document.getElementById('tsql');
const velo = 8, delay = 100, runChance = 0.01;
let hasRun = false;

const param = new Params(256, 256, 8, 8);
const aRunRight = new Loop(velo, 0, 5, [5,1,2,3]);
const aRunLeft = new Loop(-velo, 0, 6, [5,1,2,3]);

function hasQuery() {
return query.value.trim() != "";
}

function shouldSquirrel() {
return !hasRun &&
query.value.trim().toLowerCase().includes("squirrel") &&
Math.random() < runChance;
}

function maybeRunSquirrel() {
if (!shouldSquirrel()) {
return;
}
// Squirrel art from:
// http://saralara93.blogspot.com/2014/03/concept-art-part-3-squirrel.html

const nut = document.createElement("div");
nut.setAttribute("id", "nut");
document.getElementById("input").prepend(nut);
const isOdd = query.value.length%2 == 1;
const area = new Area({
figure: nut,
params: param,
startx: isOdd ? 100 : 0,
wrap: false,
});
const cycle = new Cycle(isOdd ? aRunLeft : aRunRight);
hasRun = true;
area.setVisible(true);
let timer = setInterval(() => {
if (cycle.update(area)) {
clearInterval(timer);
timer = null;
area.setVisible(false);
}
}, delay);
}

query.addEventListener("keydown", (evt) => {
if (evt.shiftKey && evt.key == "Enter") {
evt.preventDefault();
if (hasQuery()) {
qButton.click();
}
}
maybeRunSquirrel()
})

body.addEventListener("keydown", (evt) => {
Expand Down
135 changes: 135 additions & 0 deletions server/tailsql/static/sprite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Module sprite defines a basic sprite animation library.

// Animation parameters.
export class Params {
// Construct parameters for a sprite sheet of the given dimensions.
constructor(width, height, nrow, ncol) {
this.width = width;
this.height = height;
this.nrow = nrow;
this.ncol = ncol;
this.fh = height / nrow;
this.fw = width / ncol;
}
}

// An Area defines the region where an animation will play.
// The figure is an element containing the sprite sheet as a background image.
// Animation is constrained to the bounding box of the parent element of the figure.
export class Area {
#img; #params; #hcap; #vcap; #locx; #locy; #wrap;

// Initialize an area for the given figure and parameters.
// The figure is the element containing the sprite image.
// The params are a Params instance describing its parameters.
//
// Options:
// startx is the initial percentage (0..100) of the parent width,
// starty is the initial percentage (0..100) of the parent height,
// wrap is whether to wrap around at the end of a cycle.
constructor({figure, params, startx=0, starty=0, wrap=true} = {}) {
if (figure === undefined || params === undefined) {
throw new Error("missing required parameters");
}
this.#img = figure;
this.#params = params;
const box = figure.parentElement.getBoundingClientRect();
this.#hcap = 100 * params.fw/box.width;
this.#vcap = 100 * params.fh/box.height;
this.#locx = startx;
this.#locy = starty;
this.#wrap = wrap;
this.moveFigure();
}

static pin100(v, cap) {
if (v < -cap) {
return {wrap: true, next: 100-cap};
} else if (v >= (100-cap)) {
return {wrap: true, next: -cap};
}
return {wrap: false, next: v};
}

// Show or hide the figure.
setVisible(ok) {
this.#img.style.display = ok ? 'flex' : 'none';
}

// Move the specified sprite (1-indexed) into the viewport.
setFrame(row, col) {
let rowOffset = (row - 1) * this.#params.fh;
let colOffset = (col - 1) * this.#params.fw;

this.#img.style.backgroundPosition = `-${colOffset}px -${rowOffset}px`;
}

// Move the figure to the current location.
moveFigure() {
this.#img.style.left = `${this.#locx}%`;
this.#img.style.top = `${this.#locy}%`;
}

// Reset the location to the specified percentage offsets (0..100).
resetLocation(px, py) {
this.#locx = px;
this.#locy = py;
this.moveFigure();
}

// Update the location by dx, dy and report whether either direction
// reached a boundary. If a boundary was reached and the area allows
// wrapping, the update wraps; otherwise the update is discarded in that
// dimension.
updateLocation(dx, dy) {
let nx = Area.pin100(this.#locx + dx, this.#hcap);
if (!nx.wrap || this.#wrap) {
this.#locx = nx.next;
}
let ny = Area.pin100(this.#locy + dy, this.#vcap);
if (!ny.wrap || this.#wrap) {
this.#locy = ny.next;
}
return nx.wrap || ny.wrap;
}
}

// A Cycle represents the current state of an animation loop.
// Call update to advance to the next frame of the cycle.
export class Cycle {
#curf; #loop;

constructor(loop) {
this.#loop = loop;
this.#curf = 0;
}
update(area) {
let col = this.#loop.frames[this.#curf];
area.setFrame(this.#loop.row, col);
area.moveFigure()
let ok = area.updateLocation(this.#loop.vx/this.#loop.nframes, this.#loop.vy/this.#loop.nframes);

this.#curf += 1;
if (this.#curf >= this.#loop.nframes) {
this.#curf = 0;
}
return ok;
}
}

// A Loop represents a sequence of animation frames.
export class Loop {
#vx; #vy; #row; #frames;

constructor(vx, vy, row, frames) {
this.#vx = vx;
this.#vy = vy;
this.#row = row;
this.#frames = frames;
}
get vx() { return this.#vx; }
get vy() { return this.#vy; }
get row() { return this.#row; }
get frames() { return this.#frames; }
get nframes() { return this.#frames.length; }
}
9 changes: 9 additions & 0 deletions server/tailsql/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,12 @@ div.action {
.action .ctrl:-webkit-details-marker {
display: none;
}

#nut {
transform: scale(1.5);
position: relative;
height: 32px;
width: 32px;
background: url('/static/nut.png') 0px 0px;
display: none;
}
2 changes: 1 addition & 1 deletion server/tailsql/ui.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@
</table></div>
{{end -}}

<script src="/static/script.js"></script>
<script type="module" src="/static/script.js"></script>
</body>
</html>

0 comments on commit c87a5ec

Please sign in to comment.