Skip to content

Commit bd0650e

Browse files
committed
Migrate gh-pages to GuillocheJS v2 engine
Replace old guiloche-animator.js with new ES6 class-based engine: - bezier-easing.js + guilloche.js (new engine) - demo.js (dat.gui controls, animation, preview, export) - HiDPI/Retina support, single-stroke rendering, pre-computed animation - dat.gui upgraded to 0.7.9 - Removed stats.js, inline JS, and preview canvas hack
1 parent 86befb1 commit bd0650e

6 files changed

Lines changed: 834 additions & 907 deletions

File tree

bezier-easing.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* BezierEasing - use bezier curve for transition easing function
3+
* Inspired from Firefox's nsSMILKeySpline.cpp
4+
* Usage:
5+
* const spline = BezierEasing(0.25, 0.1, 0.25, 1.0);
6+
* spline(x) => returns the easing value | x must be in [0, 1] range
7+
*/
8+
(function (root, factory) {
9+
if (typeof module === 'object' && module.exports) {
10+
module.exports = factory();
11+
} else {
12+
root.BezierEasing = factory();
13+
}
14+
}(typeof self !== 'undefined' ? self : this, function () {
15+
16+
function BezierEasing(mX1, mY1, mX2, mY2) {
17+
if (!(this instanceof BezierEasing)) return new BezierEasing(mX1, mY1, mX2, mY2);
18+
19+
const A = (a1, a2) => 1.0 - 3.0 * a2 + 3.0 * a1;
20+
const B = (a1, a2) => 3.0 * a2 - 6.0 * a1;
21+
const C = (a1) => 3.0 * a1;
22+
23+
const calcBezier = (t, a1, a2) =>
24+
((A(a1, a2) * t + B(a1, a2)) * t + C(a1)) * t;
25+
26+
const getSlope = (t, a1, a2) =>
27+
3.0 * A(a1, a2) * t * t + 2.0 * B(a1, a2) * t + C(a1);
28+
29+
function getTForX(x) {
30+
let guessT = x;
31+
for (let i = 0; i < 4; ++i) {
32+
const slope = getSlope(guessT, mX1, mX2);
33+
if (slope === 0.0) return guessT;
34+
guessT -= (calcBezier(guessT, mX1, mX2) - x) / slope;
35+
}
36+
return guessT;
37+
}
38+
39+
return function (x) {
40+
if (mX1 === mY1 && mX2 === mY2) return x;
41+
return calcBezier(getTForX(x), mY1, mY2);
42+
};
43+
}
44+
45+
BezierEasing.css = {
46+
'ease': BezierEasing(0.25, 0.1, 0.25, 1.0),
47+
'linear': BezierEasing(0.00, 0.0, 1.00, 1.0),
48+
'ease-in': BezierEasing(0.42, 0.0, 1.00, 1.0),
49+
'ease-out': BezierEasing(0.00, 0.0, 0.58, 1.0),
50+
'ease-in-out': BezierEasing(0.42, 0.0, 0.58, 1.0),
51+
};
52+
53+
return BezierEasing;
54+
}));

demo.js

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* GuillocheJS Interactive Demo
3+
* dat.gui panel provides sliders for all parameters.
4+
*/
5+
6+
const container = document.getElementById('guilloche');
7+
8+
const guilloche = new GuillocheJS(container, {
9+
figure: {
10+
majorR: 40,
11+
minorR: 0.25,
12+
steps: Math.round(Math.random() * 2400) || 1200,
13+
radius: 25,
14+
angle: 1,
15+
amplitude: 4.5,
16+
multiplier: 2,
17+
},
18+
appearance: {
19+
lineColor: '#000000',
20+
backgroundColor: '#FFFFFF',
21+
lineWidth: 0.5,
22+
type: 'Hypotrochoid',
23+
},
24+
});
25+
26+
guilloche.clear();
27+
guilloche.draw();
28+
29+
// -- dat.gui Control Panel --
30+
31+
const params = {
32+
majorR: guilloche.figure.majorR,
33+
minorR: guilloche.figure.minorR,
34+
steps: guilloche.figure.steps,
35+
radius: guilloche.figure.radius,
36+
angle: guilloche.figure.angle,
37+
amplitude: guilloche.figure.amplitude,
38+
multiplier: guilloche.figure.multiplier,
39+
lineColor: guilloche.appearance.lineColor,
40+
backgroundColor: guilloche.appearance.backgroundColor,
41+
lineWidth: guilloche.appearance.lineWidth,
42+
opacity: guilloche.appearance.opacity,
43+
type: guilloche.appearance.type,
44+
};
45+
46+
const gui = new dat.GUI({ width: 280 });
47+
48+
const figureFolder = gui.addFolder('Figure');
49+
figureFolder.add(params, 'majorR', 0, 200).step(0.1).name('Major R').onChange(updateFigure);
50+
figureFolder.add(params, 'minorR', 0.01, 100).step(0.01).name('Minor R').onChange(updateFigure);
51+
figureFolder.add(params, 'steps', 10, 5000).step(1).name('Steps').onChange(updateFigure);
52+
figureFolder.add(params, 'radius', 0, 100).step(0.1).name('Radius').onChange(updateFigure);
53+
figureFolder.add(params, 'angle', 0, 10).step(0.01).name('Angle').onChange(updateFigure);
54+
figureFolder.add(params, 'amplitude', 0, 20).step(0.1).name('Amplitude').onChange(updateFigure);
55+
figureFolder.add(params, 'multiplier', 0, 10).step(0.1).name('Multiplier').onChange(updateFigure);
56+
figureFolder.open();
57+
58+
const appearanceFolder = gui.addFolder('Appearance');
59+
appearanceFolder.addColor(params, 'lineColor').name('Line Color').onChange(updateAppearance);
60+
appearanceFolder.addColor(params, 'backgroundColor').name('Background').onChange(updateAppearance);
61+
appearanceFolder.add(params, 'lineWidth', 0.1, 5).step(0.1).name('Line Width').onChange(updateAppearance);
62+
appearanceFolder.add(params, 'opacity', 0, 1).step(0.01).name('Opacity').onChange(updateAppearance);
63+
appearanceFolder.add(params, 'type', ['Hypotrochoid', 'Epitrochoid', 'Hypocycloid']).name('Type').onChange(updateAppearance);
64+
appearanceFolder.open();
65+
66+
function updateFigure() {
67+
guilloche.setFigure({
68+
majorR: params.majorR,
69+
minorR: params.minorR,
70+
steps: params.steps,
71+
radius: params.radius,
72+
angle: params.angle,
73+
amplitude: params.amplitude,
74+
multiplier: params.multiplier,
75+
});
76+
redrawPreview();
77+
}
78+
79+
function updateAppearance() {
80+
guilloche.setAppearance({
81+
lineColor: params.lineColor,
82+
backgroundColor: params.backgroundColor,
83+
lineWidth: params.lineWidth,
84+
opacity: params.opacity,
85+
type: params.type,
86+
});
87+
redrawPreview();
88+
}
89+
90+
// -- Target State (animation destination) --
91+
92+
const target = {
93+
majorR: 80,
94+
minorR: 5,
95+
steps: 2000,
96+
radius: 40,
97+
angle: 2,
98+
amplitude: 8,
99+
multiplier: 4,
100+
showPreview: true,
101+
previewColor: '#0066ff',
102+
previewOpacity: 0.5,
103+
};
104+
105+
const targetFolder = gui.addFolder('Target (end state)');
106+
targetFolder.add(target, 'majorR', 0, 200).step(0.1).name('Major R').onChange(redrawPreview);
107+
targetFolder.add(target, 'minorR', 0.01, 100).step(0.01).name('Minor R').onChange(redrawPreview);
108+
targetFolder.add(target, 'steps', 10, 5000).step(1).name('Steps').onChange(redrawPreview);
109+
targetFolder.add(target, 'radius', 0, 100).step(0.1).name('Radius').onChange(redrawPreview);
110+
targetFolder.add(target, 'angle', 0, 10).step(0.01).name('Angle').onChange(redrawPreview);
111+
targetFolder.add(target, 'amplitude', 0, 20).step(0.1).name('Amplitude').onChange(redrawPreview);
112+
targetFolder.add(target, 'multiplier', 0, 10).step(0.1).name('Multiplier').onChange(redrawPreview);
113+
targetFolder.add(target, 'showPreview').name('Show Preview').onChange(redrawPreview);
114+
targetFolder.addColor(target, 'previewColor').name('Preview Color').onChange(redrawPreview);
115+
targetFolder.add(target, 'previewOpacity', 0.05, 0.5).step(0.05).name('Preview Alpha').onChange(redrawPreview);
116+
targetFolder.open();
117+
118+
function getTargetFigure() {
119+
return {
120+
majorR: target.majorR,
121+
minorR: target.minorR,
122+
steps: target.steps,
123+
radius: target.radius,
124+
angle: target.angle,
125+
amplitude: target.amplitude,
126+
multiplier: target.multiplier,
127+
};
128+
}
129+
130+
function redrawPreview() {
131+
guilloche.clear();
132+
guilloche.draw();
133+
if (target.showPreview && !guilloche.animating) {
134+
guilloche.drawPreview(getTargetFigure(), target.previewColor, target.previewOpacity);
135+
}
136+
}
137+
138+
// Initial preview
139+
redrawPreview();
140+
141+
// -- Utilities --
142+
143+
const types = ['Hypotrochoid', 'Epitrochoid', 'Hypocycloid'];
144+
145+
function rand(min, max) {
146+
return min + Math.random() * (max - min);
147+
}
148+
149+
function randomFigure() {
150+
return {
151+
majorR: Math.round(rand(10, 120) * 10) / 10,
152+
minorR: Math.round(rand(0.1, 30) * 100) / 100,
153+
steps: Math.round(rand(200, 3000)),
154+
radius: Math.round(rand(2, 60) * 10) / 10,
155+
angle: Math.round(rand(0.2, 4) * 100) / 100,
156+
amplitude: Math.round(rand(1, 12) * 10) / 10,
157+
multiplier: Math.round(rand(1, 6) * 10) / 10,
158+
};
159+
}
160+
161+
// -- Actions --
162+
163+
const actions = {};
164+
165+
actions.Randomize = function () {
166+
const fig = randomFigure();
167+
Object.assign(params, fig);
168+
params.type = types[Math.floor(Math.random() * types.length)];
169+
updateFigure();
170+
updateAppearance();
171+
gui.updateDisplay();
172+
};
173+
174+
actions['Randomize Target'] = function () {
175+
const fig = randomFigure();
176+
Object.assign(target, fig);
177+
gui.updateDisplay();
178+
redrawPreview();
179+
};
180+
181+
actions['Export PNG (4K)'] = function () {
182+
guilloche.exportPNG(4096).then((blob) => {
183+
const url = URL.createObjectURL(blob);
184+
const link = document.createElement('a');
185+
link.download = 'guilloche-' + Date.now() + '.png';
186+
link.href = url;
187+
link.click();
188+
URL.revokeObjectURL(url);
189+
});
190+
};
191+
192+
// -- Animation --
193+
194+
const animParams = {
195+
duration: 3000,
196+
easing: 'ease-in-out',
197+
direction: 'alternate',
198+
iterations: 0,
199+
timeBetween: 500,
200+
};
201+
202+
const animFolder = gui.addFolder('Animation');
203+
animFolder.add(animParams, 'duration', 500, 20000).step(100).name('Duration (ms)');
204+
animFolder.add(animParams, 'easing', [
205+
'linear', 'ease', 'ease-in', 'ease-out', 'ease-in-out', 'perlin'
206+
]).name('Easing');
207+
animFolder.add(animParams, 'direction', [
208+
'normal', 'alternate', 'alternate-reverse'
209+
]).name('Direction');
210+
animFolder.add(animParams, 'iterations', 0, 20).step(1).name('Iterations (0=inf)');
211+
animFolder.add(animParams, 'timeBetween', 0, 5000).step(100).name('Pause Between');
212+
animFolder.open();
213+
214+
// Throttled GUI sync — update sliders at ~12fps instead of 60fps
215+
let lastGuiUpdate = 0;
216+
function syncGui(fig) {
217+
const now = performance.now();
218+
if (now - lastGuiUpdate < 80) return;
219+
lastGuiUpdate = now;
220+
params.majorR = fig.majorR;
221+
params.minorR = fig.minorR;
222+
params.steps = Math.round(fig.steps);
223+
params.radius = fig.radius;
224+
params.angle = fig.angle;
225+
params.amplitude = fig.amplitude;
226+
params.multiplier = fig.multiplier;
227+
gui.updateDisplay();
228+
}
229+
230+
actions.Animate = function () {
231+
guilloche.animate(getTargetFigure(), {
232+
duration: animParams.duration,
233+
easing: animParams.easing,
234+
direction: animParams.direction,
235+
iterations: animParams.iterations,
236+
timeBetween: animParams.timeBetween,
237+
delay: 0,
238+
}, syncGui);
239+
};
240+
241+
actions.Stop = function () {
242+
guilloche.stop();
243+
const fig = guilloche.figure;
244+
Object.assign(params, fig);
245+
params.steps = Math.round(params.steps);
246+
gui.updateDisplay();
247+
redrawPreview();
248+
};
249+
250+
gui.add(actions, 'Animate');
251+
gui.add(actions, 'Stop');
252+
gui.add(actions, 'Randomize');
253+
gui.add(actions, 'Randomize Target');
254+
gui.add(actions, 'Export PNG (4K)');

0 commit comments

Comments
 (0)