feat: Kinetic Text Tags letters ({bt} {fi} {sc} {rotat} {glitch}) use the library's own formulas; Flatten; screen 3D
Each letter is moved by one animation loop with the formulas of BounceText, FadeInText, ScareText and GlitchText (fi follows the text speed, bt keeps its phase while a line is typed out, glitch cuts the text into strips that jump sideways). Flatten and `at Flatten` isolate their children as one picture, screens get the 3D turns, matrixtransform and blend of their widgets, and RotateMatrix/OffsetMatrix/ScaleMatrix exist in Python. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5ff43038ae
commit
ea351e44b3
5 files changed
+189
-23
No files matched your search
@@ -10,6 +10,7 @@ import { fastPoint, interpolate, parseText, plainText, renderText, waitPoints }
|
|||||||
import { languageName, rememberLanguage, type Translator } from "./i18n";
|
import { languageName, rememberLanguage, type Translator } from "./i18n";
|
||||||
import { naniText } from "./naninovel";
|
import { naniText } from "./naninovel";
|
||||||
import { Displayable, ScreenManager, TransformRef, type ScreenHost } from "./screens";
|
import { Displayable, ScreenManager, TransformRef, type ScreenHost } from "./screens";
|
||||||
|
import { textSettings } from "./texteffects";
|
||||||
import { containerClass, displayableClass, makeRender, pygameNamespace } from "./render";
|
import { containerClass, displayableClass, makeRender, pygameNamespace } from "./render";
|
||||||
import { musicRoomClass } from "./musicroom";
|
import { musicRoomClass } from "./musicroom";
|
||||||
import { pythonModules } from "./pymodules";
|
import { pythonModules } from "./pymodules";
|
||||||
@@ -1664,6 +1665,7 @@ export class Engine {
|
|||||||
const waits = waitPoints(parsed).filter((w) => w.at > from);
|
const waits = waitPoints(parsed).filter((w) => w.at > from);
|
||||||
let shown = from;
|
let shown = from;
|
||||||
const cps = this.settings.cps;
|
const cps = this.settings.cps;
|
||||||
|
textSettings.cps = cps; // {fi} letters fade in at the text speed
|
||||||
if (instant || cps <= 0 || this.skip) {
|
if (instant || cps <= 0 || this.skip) {
|
||||||
renderText(target, parsed, total, this.baseSize);
|
renderText(target, parsed, total, this.baseSize);
|
||||||
target.append(this.ctc);
|
target.append(this.ctc);
|
||||||
|
|||||||
@@ -372,19 +372,7 @@
|
|||||||
|
|
||||||
/* Kinetic Text Tags: {bt}, {sc}, {fi}, {rotat} move each letter on its own */
|
/* Kinetic Text Tags: {bt}, {sc}, {fi}, {rotat} move each letter on its own */
|
||||||
.pg-fx { display: inline-block; white-space: pre; }
|
.pg-fx { display: inline-block; white-space: pre; }
|
||||||
.pg-fx-bt { animation: pg-fx-bounce 0.9s ease-in-out infinite; }
|
|
||||||
.pg-fx-sc { animation: pg-fx-scare 0.12s linear infinite; }
|
|
||||||
.pg-fx-fi { animation: pg-fx-in 0.6s ease-out both; }
|
|
||||||
.pg-fx-rotat { animation: pg-fx-spin 2s linear infinite; }
|
|
||||||
.pg-fx-glitch { animation: pg-fx-glitch 0.35s steps(2, end) infinite; text-shadow: 1px 0 #f0f, -1px 0 #0ff; }
|
|
||||||
.pg-fx-wave { animation: pg-fx-wave 1.2s ease-in-out infinite; }
|
|
||||||
@keyframes pg-fx-glitch { 20% { transform: translate(-1px, 1px); } 40% { transform: translate(1px, -1px); } 60% { transform: translate(-2px, 0); } 80% { transform: translate(2px, 1px); } }
|
|
||||||
@keyframes pg-fx-wave { 50% { transform: translateY(-4px); } }
|
|
||||||
/* SnowBlossom: pictures that fall and drift while the scene is shown */
|
/* SnowBlossom: pictures that fall and drift while the scene is shown */
|
||||||
.pg-snow { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
.pg-snow { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
|
||||||
.pg-snow > * { position: absolute; top: -10%; animation: pg-snow-fall linear infinite; }
|
.pg-snow > * { position: absolute; top: -10%; animation: pg-snow-fall linear infinite; }
|
||||||
@keyframes pg-snow-fall { from { transform: translate(0, 0); } to { transform: translate(var(--drift, 40px), 120vh); } }
|
@keyframes pg-snow-fall { from { transform: translate(0, 0); } to { transform: translate(var(--drift, 40px), 120vh); } }
|
||||||
@keyframes pg-fx-bounce { 50% { transform: translateY(calc(var(--fx, 6) * -1px)); } }
|
|
||||||
@keyframes pg-fx-scare { 25% { transform: translate(1px, -1px); } 75% { transform: translate(-1px, 1px); } }
|
|
||||||
@keyframes pg-fx-in { from { opacity: 0; transform: translateY(8px); } }
|
|
||||||
@keyframes pg-fx-spin { to { transform: rotate(360deg); } }
|
|
||||||
@@ -9,6 +9,7 @@ import { galleryClass } from "./gallery";
|
|||||||
import { parseExpression } from "./pyparse";
|
import { parseExpression } from "./pyparse";
|
||||||
import { movieDisplayable } from "./movie";
|
import { movieDisplayable } from "./movie";
|
||||||
import { RenderHost, isDisplayableInstance } from "./render";
|
import { RenderHost, isDisplayableInstance } from "./render";
|
||||||
|
import { Matrix4, blendMode } from "./rendertransform";
|
||||||
import { strftime } from "./pymodules";
|
import { strftime } from "./pymodules";
|
||||||
import { OBJECT, PyClass, PyDict, PyInstance, callableOf, nativeMethod, pyEq, pyStr, registerNativeClass, truthy, type Python } from "./python";
|
import { OBJECT, PyClass, PyDict, PyInstance, callableOf, nativeMethod, pyEq, pyStr, registerNativeClass, truthy, type Python } from "./python";
|
||||||
import type { AtSpec, Expr } from "./types";
|
import type { AtSpec, Expr } from "./types";
|
||||||
@@ -1184,6 +1185,13 @@ export class ScreenManager {
|
|||||||
const name = pyStr(a).replace(/\[([A-Za-z_][\w.]*)\]/g, (_m, source: string) => pyStr(this.host.py.eval(parseExpression(source))));
|
const name = pyStr(a).replace(/\[([A-Za-z_][\w.]*)\]/g, (_m, source: string) => pyStr(this.host.py.eval(parseExpression(source))));
|
||||||
return this.displayable(name, depth + 1);
|
return this.displayable(name, depth + 1);
|
||||||
}
|
}
|
||||||
|
case "flatten": { // the children are drawn as one picture: opacity and filters then act on the whole
|
||||||
|
const group = document.createElement("div");
|
||||||
|
group.style.isolation = "isolate";
|
||||||
|
const child = this.displayable(a, depth + 1);
|
||||||
|
if (child) group.append(child);
|
||||||
|
return group;
|
||||||
|
}
|
||||||
case "animation": { // pictures that take turns; the last one stays when it has no time
|
case "animation": { // pictures that take turns; the last one stays when it has no time
|
||||||
const box = document.createElement("div");
|
const box = document.createElement("div");
|
||||||
const pictures: HTMLElement[] = [];
|
const pictures: HTMLElement[] = [];
|
||||||
@@ -1591,7 +1599,14 @@ export class ScreenManager {
|
|||||||
if (p.xoffset || p.yoffset) parts.push(`translate(${p.xoffset ?? 0}px, ${p.yoffset ?? 0}px)`);
|
if (p.xoffset || p.yoffset) parts.push(`translate(${p.xoffset ?? 0}px, ${p.yoffset ?? 0}px)`);
|
||||||
if (xz !== 1 || yz !== 1) parts.push(`scale(${xz}, ${yz})`);
|
if (xz !== 1 || yz !== 1) parts.push(`scale(${xz}, ${yz})`);
|
||||||
if (p.rotate) parts.push(`rotate(${p.rotate}deg)`);
|
if (p.rotate) parts.push(`rotate(${p.rotate}deg)`);
|
||||||
|
// 3D turns about the widget's centre (the default matrixanchor) are Ren'Py's own matrices
|
||||||
|
if (p.xrotate || p.yrotate || p.zrotate || p.zpos) {
|
||||||
|
const turn = Matrix4.offset(0, 0, p.zpos ?? 0).mul(Matrix4.rotate(p.xrotate ?? 0, p.yrotate ?? 0, p.zrotate ?? 0));
|
||||||
|
parts.unshift(turn.toCss());
|
||||||
|
}
|
||||||
|
if (p.matrixtransform instanceof Matrix4) parts.unshift(p.matrixtransform.toCss());
|
||||||
el.style.transform = parts.join(" ");
|
el.style.transform = parts.join(" ");
|
||||||
|
el.style.mixBlendMode = blendMode(p);
|
||||||
if (p.alpha !== undefined) el.style.opacity = String(p.alpha);
|
if (p.alpha !== undefined) el.style.opacity = String(p.alpha);
|
||||||
if (p.rounded !== undefined) { // RoundedCorners(radius): one radius or four (the game's own shader)
|
if (p.rounded !== undefined) { // RoundedCorners(radius): one radius or four (the game's own shader)
|
||||||
const radius = Array.isArray(p.rounded) ? p.rounded.map((r: number) => `${r}px`).join(" ") : `${p.rounded}px`;
|
const radius = Array.isArray(p.rounded) ? p.rounded.map((r: number) => `${r}px`).join(" ") : `${p.rounded}px`;
|
||||||
@@ -1600,6 +1615,7 @@ export class ScreenManager {
|
|||||||
}
|
}
|
||||||
applyFilters(el, p);
|
applyFilters(el, p);
|
||||||
};
|
};
|
||||||
|
if (specs.some((spec) => spec.name === "Flatten")) el.style.isolation = "isolate"; // `at Flatten`: the widget is one picture
|
||||||
for (const spec of specs) {
|
for (const spec of specs) {
|
||||||
Object.assign(live, spec.props ?? {}, spec.name ? this.host.transform(spec.name) ?? {} : {});
|
Object.assign(live, spec.props ?? {}, spec.name ? this.host.transform(spec.name) ?? {} : {});
|
||||||
}
|
}
|
||||||
@@ -2027,7 +2043,10 @@ export class ScreenManager {
|
|||||||
out.Fixed = shape("composite");
|
out.Fixed = shape("composite");
|
||||||
out.Composite = shape("composite");
|
out.Composite = shape("composite");
|
||||||
out.LiveComposite = shape("composite");
|
out.LiveComposite = shape("composite");
|
||||||
out.Flatten = (child: any) => child; // flattening only changes how the browser composites, not what is drawn
|
out.Flatten = shape("flatten");
|
||||||
|
out.RotateMatrix = (x: number, y: number, z: number) => Matrix4.rotate(x, y, z);
|
||||||
|
out.OffsetMatrix = (x: number, y: number, z: number) => Matrix4.offset(x, y, z);
|
||||||
|
out.ScaleMatrix = (x: number, y: number, z: number) => Matrix4.scale(x, y, z);
|
||||||
out.AlphaMask = shape("alphamask");
|
out.AlphaMask = shape("alphamask");
|
||||||
out.TrackCursor = shape("trackcursor");
|
out.TrackCursor = shape("trackcursor");
|
||||||
out.PreviewSlowText = shape("text");
|
out.PreviewSlowText = shape("text");
|
||||||
|
|||||||
+40
-10
@@ -1,6 +1,7 @@
|
|||||||
// Ren'Py text tags → styled segments, plus [variable] interpolation.
|
// Ren'Py text tags → styled segments, plus [variable] interpolation.
|
||||||
|
|
||||||
import { pyStr } from "./python";
|
import { pyStr } from "./python";
|
||||||
|
import * as fx from "./texteffects";
|
||||||
|
|
||||||
export interface Style {
|
export interface Style {
|
||||||
b?: boolean;
|
b?: boolean;
|
||||||
@@ -12,6 +13,8 @@ export interface Style {
|
|||||||
/** Per-letter motion from a custom tag: bt (bounce), sc (shake), fi (fade in), rotat (spin). */
|
/** Per-letter motion from a custom tag: bt (bounce), sc (shake), fi (fade in), rotat (spin). */
|
||||||
fx?: string;
|
fx?: string;
|
||||||
fxArg?: string;
|
fxArg?: string;
|
||||||
|
/** One id per opening tag, so the letters of one tag are counted together. */
|
||||||
|
fxId?: number;
|
||||||
/** The tag that opened this style, so its closing tag finds it again. */
|
/** The tag that opened this style, so its closing tag finds it again. */
|
||||||
tag?: string;
|
tag?: string;
|
||||||
}
|
}
|
||||||
@@ -47,6 +50,8 @@ export function interpolate(text: string, lookup: (expr: string) => any,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fxCounter = 0;
|
||||||
|
|
||||||
export function parseText(text: string): ParsedText {
|
export function parseText(text: string): ParsedText {
|
||||||
const segments: Segment[] = [];
|
const segments: Segment[] = [];
|
||||||
const stack: Style[] = [{}];
|
const stack: Style[] = [{}];
|
||||||
@@ -100,7 +105,7 @@ export function parseText(text: string): ParsedText {
|
|||||||
stack.push({ ...top, size: value, tag: name });
|
stack.push({ ...top, size: value, tag: name });
|
||||||
break;
|
break;
|
||||||
case "bt": case "sc": case "fi": case "rotat": case "glitch": case "wave": // letters that move (text tags of the games)
|
case "bt": case "sc": case "fi": case "rotat": case "glitch": case "wave": // letters that move (text tags of the games)
|
||||||
stack.push({ ...top, fx: name, fxArg: value, tag: name });
|
stack.push({ ...top, fx: name, fxArg: value, fxId: ++fxCounter, tag: name });
|
||||||
break;
|
break;
|
||||||
case "w":
|
case "w":
|
||||||
segments.push({ kind: "wait", seconds: value ? Number(value) : null });
|
segments.push({ kind: "wait", seconds: value ? Number(value) : null });
|
||||||
@@ -152,6 +157,7 @@ function splitTag(tag: string): [string, string] {
|
|||||||
/** Render the first `visible` characters of parsed text into `el`. */
|
/** Render the first `visible` characters of parsed text into `el`. */
|
||||||
export function renderText(el: HTMLElement, parsed: ParsedText, visible: number, baseSize: number) {
|
export function renderText(el: HTMLElement, parsed: ParsedText, visible: number, baseSize: number) {
|
||||||
el.textContent = "";
|
el.textContent = "";
|
||||||
|
const counters = new Map<number, number>();
|
||||||
let left = visible;
|
let left = visible;
|
||||||
for (const seg of parsed.segments) {
|
for (const seg of parsed.segments) {
|
||||||
if (seg.kind !== "text") continue;
|
if (seg.kind !== "text") continue;
|
||||||
@@ -160,20 +166,44 @@ export function renderText(el: HTMLElement, parsed: ParsedText, visible: number,
|
|||||||
const shown = chars.length <= left ? seg.text : chars.slice(0, left).join("");
|
const shown = chars.length <= left ? seg.text : chars.slice(0, left).join("");
|
||||||
left -= chars.length;
|
left -= chars.length;
|
||||||
const st = seg.style;
|
const st = seg.style;
|
||||||
if (st.fx) { // every letter moves on its own, one after another
|
if (st.fx) {
|
||||||
const amount = Number.parseFloat(st.fxArg ?? "");
|
fx.markStart(el);
|
||||||
[...shown].forEach((letter, index) => {
|
const decorate = (piece: HTMLElement) => {
|
||||||
const piece = document.createElement("span");
|
|
||||||
piece.textContent = letter;
|
|
||||||
piece.className = `pg-fx pg-fx-${st.fx}`;
|
|
||||||
piece.style.animationDelay = `${(index * 0.07).toFixed(2)}s`;
|
|
||||||
if (Number.isFinite(amount)) piece.style.setProperty("--fx", String(amount));
|
|
||||||
if (st.b) piece.style.fontWeight = "bold";
|
if (st.b) piece.style.fontWeight = "bold";
|
||||||
if (st.i) piece.style.fontStyle = "italic";
|
if (st.i) piece.style.fontStyle = "italic";
|
||||||
if (st.color) piece.style.color = st.color;
|
if (st.color) piece.style.color = st.color;
|
||||||
if (st.size) piece.style.fontSize = sizeValue(st.size, baseSize);
|
if (st.size) piece.style.fontSize = sizeValue(st.size, baseSize);
|
||||||
|
};
|
||||||
|
if (st.fx === "glitch") { // the effect acts on the whole run of text
|
||||||
|
const run = document.createElement("span");
|
||||||
|
run.className = "pg-fx";
|
||||||
|
run.textContent = shown;
|
||||||
|
decorate(run);
|
||||||
|
el.append(run);
|
||||||
|
fx.glitch(run, st.fxArg ?? "");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const letter of [...shown]) {
|
||||||
|
const piece = document.createElement("span");
|
||||||
|
piece.textContent = letter;
|
||||||
|
piece.className = "pg-fx";
|
||||||
|
decorate(piece);
|
||||||
el.append(piece);
|
el.append(piece);
|
||||||
});
|
const id = st.fxId ?? 0;
|
||||||
|
const arg = st.fxArg ?? "";
|
||||||
|
if (st.fx === "bt") {
|
||||||
|
const n = counters.get(id) ?? 0;
|
||||||
|
counters.set(id, n + 1);
|
||||||
|
fx.bounce(piece, arg, n);
|
||||||
|
} else if (st.fx === "fi") {
|
||||||
|
if (letter === " ") continue; // blanks are not counted, nor faded
|
||||||
|
const first = fx.split(arg)[0];
|
||||||
|
const n = counters.get(id) ?? (first ? parseInt(first, 10) : 0);
|
||||||
|
counters.set(id, n + 1);
|
||||||
|
fx.fadeIn(piece, arg, n);
|
||||||
|
} else if (st.fx === "sc") fx.scare(piece, arg);
|
||||||
|
else if (st.fx === "rotat") fx.rotation(piece, arg);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!st.b && !st.i && !st.u && !st.s && !st.color && !st.size) {
|
if (!st.b && !st.i && !st.u && !st.s && !st.color && !st.size) {
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Per-letter text effects of the Kinetic Text Tags library and its relatives ({bt} {fi} {sc} {rotat} {glitch}), with
|
||||||
|
// the formulas of the library's own displayables (BounceText, FadeInText, ScareText, GlitchText). Each letter is an
|
||||||
|
// element that a single animation loop moves for as long as the element stays on the page.
|
||||||
|
|
||||||
|
/** Text speed in characters per second (0 = instant), as the player's settings have it. */
|
||||||
|
export const textSettings = { cps: 0 };
|
||||||
|
|
||||||
|
interface Motion {
|
||||||
|
el: HTMLElement;
|
||||||
|
update(t: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const live = new Set<Motion>();
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
function tick() {
|
||||||
|
const now = performance.now();
|
||||||
|
for (const motion of [...live]) {
|
||||||
|
if (!motion.el.isConnected) { live.delete(motion); continue; }
|
||||||
|
const start = Number(motion.el.closest<HTMLElement>("[data-fx-start]")?.dataset.fxStart ?? now);
|
||||||
|
motion.update((now - start) / 1000);
|
||||||
|
}
|
||||||
|
if (live.size) requestAnimationFrame(tick);
|
||||||
|
else ticking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(motion: Motion) {
|
||||||
|
live.add(motion);
|
||||||
|
motion.update(0);
|
||||||
|
if (!ticking) { ticking = true; requestAnimationFrame(tick); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The time zero of the effects inside `root`: set once, so a text that is typed out letter by letter keeps its phase. */
|
||||||
|
export function markStart(root: HTMLElement) {
|
||||||
|
if (!root.dataset.fxStart) root.dataset.fxStart = String(performance.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
export const split = (arg: string) => arg.split("-").filter((p) => p !== "");
|
||||||
|
|
||||||
|
/** `{bt=a20-p2.0-s0.5}` or `{bt=12}`: amplitude, period and speed of the wave. */
|
||||||
|
export function bounce(el: HTMLElement, arg: string, offset: number) {
|
||||||
|
let amp = 20, period = 4.0, speed = 1.0;
|
||||||
|
if (arg !== "") {
|
||||||
|
const parts = split(arg);
|
||||||
|
if (parts.length === 1 && /^\d/.test(parts[0])) amp = parseInt(parts[0], 10);
|
||||||
|
else for (const part of parts) {
|
||||||
|
if (part[0] === "a") amp = parseInt(part.slice(1), 10);
|
||||||
|
else if (part[0] === "p") period = parseFloat(part.slice(1));
|
||||||
|
else if (part[0] === "s") speed = parseFloat(part.slice(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add({ el, update: (t) => { el.style.transform = `translateY(${Math.sin(period * (t * speed + offset * -0.1)) * amp}px)`; } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `{fi=index-time-distance}`: each letter fades in and slides, one after another at the text speed. */
|
||||||
|
export function fadeIn(el: HTMLElement, arg: string, number: number) {
|
||||||
|
let fade = 5.0, slide = 100;
|
||||||
|
const parts = split(arg);
|
||||||
|
if (parts.length > 1) fade = parseFloat(parts[1]);
|
||||||
|
if (parts.length > 2) slide = parseInt(parts[2], 10);
|
||||||
|
const delay = textSettings.cps !== 0 ? number / textSettings.cps : 0;
|
||||||
|
add({ el, update: (t) => {
|
||||||
|
let alpha = 0, x = 5.0;
|
||||||
|
if (t > delay) {
|
||||||
|
const progress = (t - delay) / fade;
|
||||||
|
alpha = Math.min(1, progress);
|
||||||
|
x = Math.max(slide - progress * slide, 0);
|
||||||
|
}
|
||||||
|
el.style.opacity = String(alpha);
|
||||||
|
el.style.transform = `translateX(${x}px)`;
|
||||||
|
} });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `{sc=5}`: every letter jitters inside a square of that size, on every frame. */
|
||||||
|
export function scare(el: HTMLElement, arg: string) {
|
||||||
|
const square = arg === "" ? 5 : parseFloat(arg);
|
||||||
|
add({ el, update: () => { el.style.transform = `translate(${(Math.random() - 0.5) * square}px, ${(Math.random() - 0.5) * square}px)`; } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `{rotat=300}`: the letter turns about its own centre at that many degrees a second. */
|
||||||
|
export function rotation(el: HTMLElement, arg: string) {
|
||||||
|
const speed = arg === "" ? 300 : parseFloat(arg);
|
||||||
|
add({ el, update: (t) => { el.style.transform = `rotate(${t * speed}deg)`; } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `{glitch=10}`: the text is cut into strips of random height and, now and then, a strip jumps sideways. */
|
||||||
|
export function glitch(el: HTMLElement, arg: string) {
|
||||||
|
const amount = arg === "" ? 10 : parseFloat(arg);
|
||||||
|
const pool: HTMLElement[] = [];
|
||||||
|
el.style.position = "relative";
|
||||||
|
let color = "";
|
||||||
|
let text = "";
|
||||||
|
add({ el, update: () => {
|
||||||
|
const h = el.offsetHeight, w = el.offsetWidth;
|
||||||
|
if (!h || !w) return;
|
||||||
|
if (!color) { // the text itself is hidden; the strips, which carry its colour, are what shows
|
||||||
|
text = el.textContent ?? "";
|
||||||
|
color = getComputedStyle(el).color;
|
||||||
|
el.style.color = "transparent";
|
||||||
|
}
|
||||||
|
let used = 0;
|
||||||
|
let y = 0;
|
||||||
|
let guard = 0;
|
||||||
|
while (y < h && guard++ < 200) {
|
||||||
|
const occurs = Math.random() * 100 < amount;
|
||||||
|
const height = occurs ? Math.round(Math.random() * 20 - 10) : Math.round(Math.random() * 10);
|
||||||
|
const offset = Math.round(Math.random() * 20 - 10);
|
||||||
|
if (height > 0) {
|
||||||
|
let strip = pool[used];
|
||||||
|
if (!strip) {
|
||||||
|
strip = document.createElement("span");
|
||||||
|
strip.className = "pg-glitch-strip";
|
||||||
|
strip.style.cssText = `position:absolute;left:0;top:0;white-space:pre;pointer-events:none;color:${color}`;
|
||||||
|
strip.textContent = text;
|
||||||
|
el.append(strip);
|
||||||
|
pool.push(strip);
|
||||||
|
}
|
||||||
|
used++;
|
||||||
|
strip.style.visibility = "visible";
|
||||||
|
strip.style.clipPath = `inset(${y}px 0 ${Math.max(0, h - y - height)}px 0)`;
|
||||||
|
strip.style.transform = `translateX(${occurs ? offset : 0}px)`;
|
||||||
|
}
|
||||||
|
y += Math.abs(height) || 1;
|
||||||
|
}
|
||||||
|
for (let i = used; i < pool.length; i++) pool[i].style.visibility = "hidden";
|
||||||
|
} });
|
||||||
|
}
|
||||||
Reference in new issue
Block a user