Commit 789f4e78 by PLN (Algolia)

feat: Hydra, poem search

parent e81cfe77
import React, { useRef, useEffect } from "react";
export default function HydraSynth({ source }) {
const canvasRef = useRef(null);
const containerRef = useRef(null);
const aspectRatio = 16/9;
class HydraSynth extends React.Component {
constructor(props) {
super(props);
this.width = props.width;
this.height = props.height;
this.source= props.source;
// FIXME: Webgl not supported if I use this??
// this.canvasRef = props.canvasRef;
}
useEffect(() => {
if (!canvasRef.current) return;
// Dynamically import hydra-synth (client-side only)
import('hydra-synth').then((HydraModule) => {
const canvas = canvasRef.current;
// Adjust canvas size to maintain 16:9 aspect ratio based on container width
const containerWidth = containerRef.current.clientWidth;
const width = Math.min(containerWidth, 1280); // Cap at 1280px max width
const height = width / aspectRatio;
componentDidMount() {
import('hydra-synth').then((Hydra) => {
console.log(this.canvasRef);
const hydra = new Hydra.default({
// canvas element to render to. If none is supplied,
// a canvas will be created and appended to the screen
canvas: this.canvasRef,
autoLoop: true,
makeGlobal: true,
numSources: 4,
canvas.width = width;
canvas.height = height;
// Initialize hydra
const hydra = new HydraModule.default({
canvas: canvas,
detectAudio: false,
numOutputs: 4,
});
hydra.setResolution(this.width, this.height);
hydra.eval(this.source);
width: width,
height: height
});
// Sanitize and prepare the source code
const sanitizeHydraCode = (code) => {
// Remove any HTML-like tags
const sanitizedCode = code.replace(/<[^>]*>/g, '');
// Trim whitespace and remove any leading/trailing backticks
return sanitizedCode.trim().replace(/^`+|`+$/g, '');
};
// Validate and run the hydra code
const runHydraCode = (code) => {
try {
// Sanitize the code
const cleanCode = sanitizeHydraCode(code);
// Check if the code is empty after sanitization
if (!cleanCode) {
console.warn("Hydra source code is empty after sanitization.");
return;
}
render() {
return <div>
{/* <canvas ref="{this.canvasRef}" width={this.width} height={this.height} /> */}
</div>;
// Attempt to evaluate the code
hydra.eval(cleanCode);
} catch (error) {
console.error("Error running Hydra code:", error);
// Attempt to provide more context
console.log("Original source code:", code);
console.log("Sanitized code:", sanitizeHydraCode(code));
}
}
};
// Run the hydra code
runHydraCode(source);
export default HydraSynth
// Clean up function
return () => {
// Any cleanup needed for hydra
if (hydra && typeof hydra.close === 'function') {
hydra.close();
}
};
}).catch(importError => {
console.error("Failed to import Hydra module:", importError);
});
}, [source]);
return (
<div ref={containerRef} style={{ width: '100%', marginBottom: '1.5rem' }}>
<canvas
ref={canvasRef}
style={{
width: '100%',
height: 'auto',
aspectRatio: `${aspectRatio}`,
border: '1px solid #ccc',
backgroundColor: 'black'
}}
/>
</div>
);
}
......@@ -3,6 +3,7 @@ import Image from "next/image";
import styles from "./layout.module.css";
import utilStyles from "../styles/utils.module.css";
import Link from "next/link";
import Router from 'next/router'
const name = "PLN";
export const siteTitle = "PLN's Works";
......@@ -14,6 +15,7 @@ export default function Layout({ children, home }) {
return (
<div className={styles.container}>
<Head>
<script src="http://localhost:8097"></script>
<link rel="icon" href="/favicon.ico" />
<meta name="description" content={description} />
{/* Twitter */}
......@@ -70,9 +72,9 @@ export default function Layout({ children, home }) {
<main>{children}</main>
{!home && (
<div className={styles.backToHome}>
<Link href="/">
Back to home
</Link>
<div onClick={() => Router.back()} className={utilStyles.backButton}>
Go Back
</div>
</div>
)}
<footer>
......
---
---
title: "Endimanchés"
date: "2022-01-22 12:00:42"
tags: ["musique", "texte"]
lines: 28
words: 114
chars: 626
original_file: "notes.carnet/carnet_archive/Endimanchés.sqd"
---
On dit mon cher
Que la vie est trop chère 
Dis-moi pour quoi on supporte cette galère 
On dit mon frère 
Qu'la vie dans la misère 
Nous maintient à g'noux que parce qu'on l'a tolère 
Endimanchés 
On va en chier
Y'a rien qu'on fait pas quand il faut palper
Endimanchés 
On va en chier
Mais qu'est-ce qu'on ferait pas pour du papier monnaie 
Tous vêtus de noir de la tête aux pied
Tous motivés à leur faire payer 
Tous préparés on va prédater
On ne va rien leur prendre qu'ils n'aient pas volé 
Bandit manchot 
Maudit la chair 
À démarcher 
On dementait
Le démon chaud 
Veut des marchés
Il vient démonter 
Ton chaud déhanché
\ No newline at end of file
---
---
title: "Miné à police "
date: "2020-05-29 12:50:41"
lines: 35
words: 188
chars: 1172
original_file: "notes.carnet/Miné à police .sqd"
---
Amorphe je m'arrache5 des bras de Morphée4
Je morfle2. Et commence à rassembler7
Les instantanés flous5 issus de cette nuit 
La fumée et les flames, et la foule, et les cris 
Confus je les parcours en cherchant à comprendre
Qui a-t-on attaqué, qui voulait se défendre 
Comment un bâtiment s'est il mis à brûler 
Qui est mort étouffé d'un genou policier 
Peut-on vraiment prétendre que toute vie vaut une vie 
Mais quelle est l'étincelle d'où partait l'incendie ? 
Mais quelle est l'étincelle d'où partait l'incendie ? 
Des faces cagoulées5 des sirènes qui mugissent 
Des policiers armés, le visage vide et lisse 
Des façades brûlées 
Et la colère des filles
Et la haine des fils
Aux larmes se rajoutent rapidement les cris
Des armes sont brandies, et résonnent les bris 
Des vitrines de Target et du commissariat
Pendant qu'internet parle, qu'il acclame ou dénonce 
Le président des states tire un coup de semonce
Les décrits comme des thugs, 
Que l'armée shootera
J'aperçois l'étincelle d'où partent les incendies 
J'aperçois l'étincelle d'où partent les incendies 
Qui saura éteindre celle du prochain  incendie ? 
Qui saura éteindre celle du prochain incendie ?
\ No newline at end of file
......@@ -3,7 +3,7 @@ title: "Haïkus - Courts "
date: "2022-06-28 17:40:41"
color: "purple"
tags: ["poème", "texte"]
language: "Francais"
language: "Français"
lines: 108
words: 382
chars: 2260
......
---
title: "Cet Espace"
title: "Entre Toi et Moi"
date: "2022-06-28 17:40:41"
color: "purple"
tags: ["poème", "texte"]
......
---
title: "Haïkus"
date: "2022-06-28 17:40:41"
date: "2025-03-28 16:00:00"
color: "purple"
tags: ["poème", "texte"]
language: "Français"
......
......@@ -29,3 +29,5 @@ export function getAllPoemIds() {
export async function getPoemData(id) {
return getContentData("poems", id);
}
......@@ -15,6 +15,7 @@
"classnames": "^2.5.1",
"date-fns": "^3.3.1",
"gray-matter": "^4.0.3",
"hydra-synth": "^1.3.29",
"next": "^15.3.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
......
......@@ -2,7 +2,6 @@ import Image from "next/image";
import Link from "next/link";
import Head from "next/head";
import Layout from "../components/layout";
// import HydraSynth from "../components/hydra-view.js";
import utilStyles from "../styles/utils.module.css";
import { getHydrasData } from "../lib/hydras";
......@@ -32,7 +31,12 @@ export default function Hydra({ hydras }) {
/>
<p>
<i>
<a href="https://nech.pl/glitchpool">GLITCHP00L</a>, a work in which a blue bar becomes the colorful frontier
<a
href="https://nech.pl/glitchpool"
className={utilStyles.inlineLink}
>
GLITCHP00L
</a>, a work in which a blue bar becomes the colorful frontier
of s1gnal and n0ise.{" "}
</i>
</p>
......@@ -40,14 +44,18 @@ export default function Hydra({ hydras }) {
<Image
alt="GaLactic intermediary steps"
src="/images/WIP_GaLactic.png"
// layout="fill"
width={700}
height={475}
/>
<p>
<i>
Successive layers within the work{" "}
<a href="https://nech.pl/nights-galactic">GaLactic</a>, part of the
<a
href="https://nech.pl/nights-galactic"
className={utilStyles.inlineLink}
>
GaLactic
</a>, part of the
serie <b>Nuits étoilées</b>
<br />
in which I turn pictures of the night sky into animated windows on
......@@ -58,7 +66,12 @@ export default function Hydra({ hydras }) {
<br />
<h4>
All my Hydra <a href="https://git.plnech.fr/pln/Hydra">code</a> is{" "}
All my Hydra <a
href="https://git.plnech.fr/pln/Hydra"
className={utilStyles.inlineLink}
>
code
</a> is{" "}
<i>free software</i> : you are free to run it, study it, modify it or share it
as you like.
</h4>
......@@ -66,15 +79,31 @@ export default function Hydra({ hydras }) {
<br />
<h2>Some works</h2>
<ul className={utilStyles.list}>
{hydras.map(({ id, title }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/hydra/${id}`} legacyBehavior>
<h4> {title}</h4>
<div className={utilStyles.list}>
{hydras.map(({ id, title, link }) => (
<div className={utilStyles.hydraCard} key={id}>
<Link
href={`/hydra/${id}`}
className={utilStyles.listItemLink}
>
{title}
</Link>
</li>
<div className={utilStyles.poemMeta}>
<span className={utilStyles.poemMetaItem}>
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className={utilStyles.inlineLink}
>
View fullscreen
</a>
</span>
</div>
</div>
))}
</ul>
</div>
</div>
</Layout>
);
......
import SyntaxHighlighter from "react-syntax-highlighter";
import Head from "next/head";
import Layout from "../../components/layout";
// import HydraSynth from "../../components/hydra-view";
import Date from "../../components/date";
const https = require("https");
import { useRef } from "react";
import { useState } from "react";
import dynamic from "next/dynamic";
import utilStyles from "../../styles/utils.module.css";
import { getAllHydraIds, getHydraData } from "../../lib/hydras";
// Dynamically import HydraSynth with SSR disabled
const HydraSynth = dynamic(
() => import("../../components/hydra-view"),
{ ssr: false }
);
export async function getStaticProps({ params }) {
const hydraData = await getHydraData(params.id);
......@@ -35,7 +39,8 @@ export async function getStaticPaths() {
}
export default function Hydra({ hydraData, sourceCode }) {
// const canvasRef = useRef(null);
const [showCode, setShowCode] = useState(false);
return (
<Layout>
<Head>
......@@ -43,19 +48,27 @@ export default function Hydra({ hydraData, sourceCode }) {
</Head>
<article>
<h1 className={utilStyles.headingXl}>{hydraData.title}</h1>
<h3>See it running at the bottom of this page, or</h3>
<h5>
<i><a href={hydraData.link}>Run it fullscreen in your browser</a></i>
</h5>
{/* <canvas id={canvasRef}/> */}
{/* <HydraSynth width={700} height={475} */}
{/* canvasRef={canvasRef} source={hydraData.source} /> */}
{/* Hydra visualization */}
<div className={utilStyles.hydraContainer}>
<HydraSynth source={hydraData.source} />
</div>
{/* Fullscreen link */}
<div className={utilStyles.hydraLinks}>
<a href={hydraData.link} target="_blank" rel="noopener noreferrer">
View fullscreen in your browser
</a>
</div>
{/* Source code (conditionally rendered) */}
<div className={utilStyles.codeContainer}>
<SyntaxHighlighter
width="64em"
language="javascript"
wrapLongLines={true}>
{hydraData.source}
</SyntaxHighlighter>
</div>
</article>
</Layout>
);
......
......@@ -62,7 +62,7 @@ export default function Home({ posts, talks }) {
</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<Link href="/poesie/" className={utilStyles.sectionLink}>
<span>…words into <em>thoughts</em></span>
<span>…words into <em>poems</em></span>
</Link>
<p className="description">
A selection of texts scattered across note books and note apps.
......
import { useState } from 'react';
import { useState, useEffect } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import Layout from '../components/layout';
......@@ -18,20 +18,34 @@ export async function getStaticProps() {
export default function Poems({ poems }) {
const [sortBy, setSortBy] = useState('date');
const [sortOrder, setSortOrder] = useState('desc');
const [filterLanguage, setFilterLanguage] = useState('Français'); // Default language set to Français
const [filterLanguage, setFilterLanguage] = useState('Français');
const [filterTags, setFilterTags] = useState('all');
const [searchTerm, setSearchTerm] = useState('');
const [displayedPoems, setDisplayedPoems] = useState([]);
// Get unique languages and tags
const languages = ['all', ...new Set(poems.map(p => p.language || 'unknown'))];
const allTags = [...new Set(poems.flatMap(p => p.tags || []))];
const tags = ['all', ...allTags];
// Filter and sort poems
const filteredPoems = poems
// Filter and sort poems whenever filters change
useEffect(() => {
// Filter poems based on all criteria
const filtered = poems
.filter(p => filterLanguage === 'all' || p.language === filterLanguage)
.filter(p => filterTags === 'all' || (p.tags && p.tags.includes(filterTags)));
.filter(p => filterTags === 'all' || (p.tags && p.tags.includes(filterTags)))
.filter(p => {
if (!searchTerm) return true;
const sortedPoems = [...filteredPoems].sort((a, b) => {
const search = searchTerm.toLowerCase();
const titleMatch = p.title?.toLowerCase().includes(search);
const contentMatch = p.contentHtml?.toLowerCase().includes(search);
return titleMatch || contentMatch;
});
// Sort filtered poems
const sorted = [...filtered].sort((a, b) => {
if (sortBy === 'date') {
if (a.date < b.date) return sortOrder === 'desc' ? 1 : -1;
if (a.date > b.date) return sortOrder === 'desc' ? -1 : 1;
......@@ -45,6 +59,9 @@ export default function Poems({ poems }) {
return 0;
});
setDisplayedPoems(sorted);
}, [poems, sortBy, sortOrder, filterLanguage, filterTags, searchTerm]);
return (
<Layout>
<Head>
......@@ -56,36 +73,19 @@ export default function Poems({ poems }) {
<h1>Words into thoughts</h1>
<p>A selection of texts scattered across note books and note apps.</p>
<div className={utilStyles.filtersContainer}>
<div className={utilStyles.filtersGroup}>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Sort by</span>
<select
className={utilStyles.filterSelect}
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
aria-label="Sort poems by"
>
<option value="date">Date</option>
<option value="wordcount">Word count</option>
</select>
</div>
<div className={utilStyles.searchContainer}>
{/* Search bar */}
<input
type="text"
className={utilStyles.searchBar}
placeholder="Search poems by title or content..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-label="Search poems"
/>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Order</span>
<select
className={utilStyles.filterSelect}
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value)}
aria-label="Sort order"
>
<option value="desc">Newest first</option>
<option value="asc">Oldest first</option>
</select>
</div>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Language</span>
{/* Compact filter dropdowns */}
<div className={utilStyles.filterDropdowns}>
<select
className={utilStyles.filterSelect}
value={filterLanguage}
......@@ -98,10 +98,7 @@ export default function Poems({ poems }) {
</option>
))}
</select>
</div>
<div className={utilStyles.filterItem}>
<span className={utilStyles.filterLabel}>Tags</span>
<select
className={utilStyles.filterSelect}
value={filterTags}
......@@ -114,17 +111,35 @@ export default function Poems({ poems }) {
</option>
))}
</select>
</div>
<select
className={utilStyles.filterSelect}
value={`${sortBy}-${sortOrder}`}
onChange={(e) => {
const [newSortBy, newSortOrder] = e.target.value.split('-');
setSortBy(newSortBy);
setSortOrder(newSortOrder);
}}
aria-label="Sort options"
>
<option value="date-desc">Newest first</option>
<option value="date-asc">Oldest first</option>
<option value="wordcount-desc">Longest first</option>
<option value="wordcount-asc">Shortest first</option>
</select>
</div>
</div>
<div className={utilStyles.list}>
{sortedPoems.length === 0 ? (
{displayedPoems.length === 0 ? (
<p>No poems match your current filters. Try adjusting your selection.</p>
) : (
sortedPoems.map(({ id, title, date, language, tags }) => (
displayedPoems.map(({ id, title, date, language, tags }) => (
<div className={utilStyles.poemCard} key={id}>
<Link href={`/poesie/${id}`} className={utilStyles.listItemLink}>
<Link
href={`/poesie/${id}`}
className={utilStyles.listItemLink}
>
{title}
</Link>
......
......@@ -265,3 +265,103 @@
margin: 1rem auto;
}
}
.hydraContainer {
width: 100%;
margin: 2rem 0 1rem;
position: relative;
}
.hydraLinks {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
font-size: 0.95rem;
}
.hydraLinks a {
color: #0070f3;
text-decoration: none;
}
.hydraLinks a:hover {
text-decoration: underline;
}
.toggleButton {
background: none;
border: none;
color: #0070f3;
cursor: pointer;
font-size: 0.95rem;
padding: 0;
text-decoration: none;
}
.toggleButton:hover {
text-decoration: underline;
}
.codeContainer {
margin-top: 1rem;
border-radius: 8px;
overflow: hidden;
background-color: #f6f8fa;
}
.searchContainer {
display: flex;
flex-wrap: wrap;
gap: 1rem;
background-color: #f8f9fa;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1.5rem;
align-items: center;
}
.searchBar {
flex: 1;
min-width: 200px;
padding: 0.6rem 1rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.95rem;
background-color: white;
}
.searchBar:focus {
outline: none;
border-color: #0070f3;
box-shadow: 0 0 0 2px rgba(0, 112, 243, 0.2);
}
.filterDropdowns {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.filterSelect {
padding: 0.6rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9rem;
background-color: white;
min-width: 120px;
}
.hydraCard {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: 1.5rem;
margin-bottom: 1.5rem;
transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
}
.hydraCard:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment