'#0B0B0C', 't' => '#1F4B5E', 'r' => '#D40000']; const GATE_DOTS = [ [34, 86, 3.4, 'k'], [64, 86, 3.8, 'r'], [24, 86, 3.8, 'k'], [74, 86, 3.4, 'k'], [14, 86, 3.4, 'k'], [84, 86, 3.8, 't'], [34, 76, 3.4, 'k'], [64, 76, 3.4, 'k'], [24, 76, 3.4, 't'], [74, 76, 3.8, 'k'], [14, 76, 3.8, 'k'], [84, 76, 3.4, 'k'], [34, 66, 3.4, 't'], [64, 66, 3.8, 'k'], [24, 66, 3.8, 'k'], [74, 66, 3.4, 'k'], [14, 66, 3.4, 'k'], [84, 66, 3.8, 'r'], [34, 56, 3.8, 'k'], [64, 56, 3.4, 'k'], [24, 56, 3.4, 'k'], [74, 56, 3.8, 't'], [14, 56, 3.8, 'k'], [84, 56, 3.4, 'k'], [34, 46, 3.4, 'k'], [64, 46, 3.8, 'k'], [24, 46, 3.8, 'k'], [74, 46, 3.4, 'k'], [14, 46, 3.4, 't'], [84, 46, 3.8, 'k'], [44, 36, 3.8, 't'], [54, 36, 3.8, 'k'], [34, 36, 3.4, 'k'], [64, 36, 3.4, 'k'], [24, 36, 3.8, 'r'], [74, 36, 3.8, 'k'], [14, 36, 3.4, 'k'], [84, 36, 3.4, 'k'], [44, 26, 3.4, 'k'], [54, 26, 3.8, 'k'], [34, 26, 3.8, 'k'], [64, 26, 3.4, 't'], [24, 26, 3.4, 'k'], [74, 26, 3.8, 'k'], [14, 26, 3.8, 'k'], [84, 26, 3.4, 'k'], [44, 16, 3.8, 'k'], [54, 16, 3.4, 'k'], [34, 16, 3.8, 't'], [64, 16, 3.8, 'k'], [24, 16, 3.4, 'k'], [74, 16, 3.4, 'r'] ]; /** The simplified 5 by 5 gate, for anything drawn below 32 px (the brand pack's mark-small). */ const GATE_SMALL = [[32, 14, 6.5, 't'], [50, 14, 6.5, 'k'], [68, 14, 6.5, 'k'], [14, 32, 6.5, 'k'], [32, 32, 6.5, 'k'], [50, 32, 6.5, 'k'], [68, 32, 6.5, 'k'], [86, 32, 6.5, 'k'], [14, 50, 6.5, 't'], [32, 50, 6.5, 'k'], [68, 50, 6.5, 'k'], [86, 50, 6.5, 'k'], [14, 68, 6.5, 'k'], [32, 68, 6.5, 'k'], [68, 68, 6.5, 't'], [86, 68, 6.5, 'k'], [14, 86, 6.5, 'k'], [32, 86, 6.5, 'k'], [68, 86, 6.5, 'k'], [86, 86, 6.5, 'k'], [50, 78, 9, 'r']]; function gatecraft_mark_small(string $variant = 'color', string $class = 'gc-mark-sm'): string { $rev = $variant === 'reversed'; $out = ''; } /** The admin always looks like Gatecraft: these replace the organiser's colour inside the admin. Front-end branding is in Settings. */ /** The Gatecraft lockup: the mark and the wordmark. */ function gatecraft_lockup(bool $dark = false, string $href = '/admin'): string { return '' . gatecraft_mark($dark ? 'reversed' : 'color', 'brand-gc-mark') . ''; } /** Where Gatecraft lives on the web, for the "Powered by" link. Empty: the line is shown without a link. */ defined('GATECRAFT_HOME_URL') || define('GATECRAFT_HOME_URL', ''); /** * The gate as inline SVG. * level how much of the gate is filled, in dots (0 to 52, fractions allowed). Whole dots are drawn at full strength in * fill order; the next dot is filled from the bottom by the fraction (by area); the rest are faint outlines. * 52 = the full mark. * guest draw the red guest in the doorway * ring index of the dot to ring (a guest's own place on their ticket), or null * variant color (light backgrounds) or reversed (dark backgrounds) */ function gate_svg(float $level = 52, bool $guest = true, ?int $ring = null, string $variant = 'color', string $label = 'Gatecraft', string $class = 'gc-gate'): string { static $uid = 0; $rev = $variant === 'reversed'; $n = count(GATE_DOTS); $level = max(0.0, min((float)$n, $level)); $whole = (int)floor($level + 1e-9); $frac = $level - $whole; $out = ''; foreach (GATE_DOTS as $i => [$x, $y, $r, $role]) { $fill = $rev && $role !== 'r' ? '#FFFFFF' : GATE_COLORS[$role]; $base = $rev && $role === 't' ? 0.42 : 1.0; $on = $i < $whole; $op = $on ? $base : $base * 0.12; $out .= ''; if ($i === $whole && $frac > 0.001) { // The dot being filled now: solid from the bottom up, cut where its filled area equals the fraction. $h = gate_fill_height($frac) * 2 * $r; $id = 'gcg' . (++$uid) . '-' . substr(md5((string)microtime(true)), 0, 6); $out .= '' . ''; } } if ($ring !== null && isset(GATE_DOTS[$ring])) { [$x, $y, $r] = GATE_DOTS[$ring]; $out .= ''; } if ($guest) $out .= ''; return $out . ''; } /** Height, as a share of the diameter, of the bottom slice of a circle that holds $share of its area. */ function gate_fill_height(float $share): float { $lo = 0.0; $hi = 1.0; for ($k = 0; $k < 30; $k++) { $m = ($lo + $hi) / 2; $d = 1 - 2 * $m; // distance of the cut line from the centre, as a share of the radius $area = (acos($d) - $d * sqrt(1 - $d * $d)) / M_PI; if ($area < $share) $lo = $m; else $hi = $m; } return ($lo + $hi) / 2; } /** The Gatecraft mark, drawn from the same data. */ function gatecraft_mark(string $variant = 'color', string $class = 'gc-mark'): string { return gate_svg(count(GATE_DOTS), true, null, $variant, 'Gatecraft', $class); } /** Total places for an event: the places in its active ticket categories, capped by the event's or the venue's capacity. Null when there are none. */ function event_places(array $ev): ?int { $sum = (int)qv('SELECT COALESCE(SUM(capacity), 0) FROM {ticket_categories} WHERE event_id = ? AND is_active = 1 AND capacity > 0', [(int)$ev['id']]); $cap = event_cap($ev); $places = $cap !== null && $sum > 0 ? min($cap, $sum) : ($cap ?? $sum); return $places > 0 ? $places : null; } /** The living gate of an event: places, places taken, dots filled and whether the doors are open. Null when the event has no limit. */ function event_gate(array $ev, ?array $held = null): ?array { $places = event_places($ev); if (!$places) return null; $taken = min($places, ($held ?? held_counts((int)$ev['id']))['total']); $level = $taken / $places * count(GATE_DOTS); // the share of places taken, as an exact share of the gate $doorsAt = lstrtotime((string)($ev['doors_at'] ?: $ev['starts_at'])); $endAt = lstrtotime((string)($ev['ends_at'] ?: $ev['starts_at'])) + ($ev['ends_at'] ? 0 : 6 * 3600); $open = $ev['status'] === 'published' && time() >= $doorsAt && time() <= $endAt; return ['places' => $places, 'taken' => $taken, 'level' => $level, 'share' => $taken / $places, 'doors' => $open, 'full' => $taken >= $places]; } /** True when the site may leave "Powered by Gatecraft" off its public pages, tickets and emails (an add-on option). */ function powered_by_hidden(): bool { return (bool)apply_filters('gatecraft_hide_powered_by', false); } /** "Powered by Gatecraft". Off on the public site and in emails unless the site owner turns it on (guideline 10). The admin always shows it. */ function powered_by(string $variant = 'color', ?string $href = null): string { $mark = gatecraft_mark_small($variant, 'gc-pw-mark'); $inner = $mark . 'Powered by Gatecraft'; $cls = 'gc-powered gc-powered-' . ($variant === 'reversed' ? 'rev' : 'std'); $href ??= GATECRAFT_HOME_URL; if ($href === '') return '
' . $inner . '
'; $ext = preg_match('#^https?://#', $href) === 1; return '
' . $inner . '
'; } /** The same line for emails: plain text and a link, no images (many mail apps block them). */ function powered_by_email(): string { $txt = 'Powered by Gatecraft'; return GATECRAFT_HOME_URL !== '' ? '' . $txt . '' : '' . $txt . ''; } /* * Links in the footer of the ticket pages: social channels with their own icons, the website, and other links with a * general icon. Set under Tickets > Settings > Content and messages. */ const FOOTER_CHANNELS = [ 'instagram' => ['Instagram', 'social_instagram'], 'facebook' => ['Facebook', 'social_facebook'], 'x' => ['X', 'social_x'], 'linkedin' => ['LinkedIn', 'social_linkedin'], 'website' => ['Website', 'social_website'], ]; const FOOTER_LINKS_MAX = 8; /** "Label | https://address" per line (the label is optional); https:// and mailto: addresses. Returns [[label, url]] or throws the message of the first bad line. */ function footer_links_parse(string $raw): array { $out = []; foreach (preg_split('/\R/', $raw) ?: [] as $n => $line) { $line = trim($line); if ($line === '') continue; $parts = array_map('trim', explode('|', $line, 2)); [$label, $url] = count($parts) === 2 ? $parts : ['', $parts[0]]; $ok = (preg_match('#^https://[^\s<>"]+$#i', $url) && filter_var($url, FILTER_VALIDATE_URL)) || (preg_match('#^mailto:[^\s<>"]+$#i', $url) && filter_var(substr($url, 7), FILTER_VALIDATE_EMAIL)); if (!$ok) throw new \InvalidArgumentException('Line ' . (int)($n + 1) . ' of the other links needs an address that starts with https:// or mailto:'); if ($label === '') $label = str_starts_with(strtolower($url), 'mailto:') ? substr($url, 7) : (string)preg_replace('#^www\.#i', '', (string)wp_parse_url($url, PHP_URL_HOST)); $out[] = [mb_substr($label, 0, 40), $url]; } if (count($out) > FOOTER_LINKS_MAX) throw new \InvalidArgumentException('Use at most ' . (int)FOOTER_LINKS_MAX . ' other links.'); return $out; } /** The footer links in display order: [kind, label, url]. Kind is a channel, or link or mail for the other links. */ function footer_links(): array { $out = []; foreach (FOOTER_CHANNELS as $kind => [$label, $key]) { $u = (string)setting($key); if (preg_match('#^https://#', $u)) $out[] = ['kind' => $kind, 'label' => $label, 'url' => $u]; } try { $custom = footer_links_parse((string)setting('footer_links')); } catch (\InvalidArgumentException $e) { $custom = []; } foreach ($custom as [$label, $url]) $out[] = ['kind' => str_starts_with(strtolower($url), 'mailto:') ? 'mail' : 'link', 'label' => $label, 'url' => $url]; return $out; } /** The icon of a footer link: the channel's own mark, or a general one. The marks are single-colour and take the text colour. */ function footer_icon(string $kind): string { $brand = fn(string $p) => ''; $line = fn(string $p) => ''; return match ($kind) { 'instagram' => $line(''), 'facebook' => $brand(''), 'x' => $brand(''), 'linkedin' => $brand(''), 'website' => $line(''), 'mail' => $line(''), default => $line(''), }; }